Skip to content

Commit 553f9ff

Browse files
authored
Add Unit Tests (#2)
* Add unit tests * Prevent zip file from being uploaded to Lambda code
1 parent 7528449 commit 553f9ff

File tree

11 files changed

+2783
-7
lines changed

11 files changed

+2783
-7
lines changed

Makefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ test_unit: install
3737
terraform -chdir=terraform/envs/general init -reconfigure -backend=false -upgrade
3838
terraform -chdir=terraform/envs/general fmt -check
3939
terraform -chdir=terraform/envs/general validate
40+
yarn test
4041

4142
lock_terraform:
4243
terraform -chdir=terraform/envs/general providers lock -platform=windows_amd64 -platform=darwin_amd64 -platform=darwin_arm64 -platform=linux_amd64 -platform=linux_arm64

build.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ esbuild
3131
.build({
3232
...commonParams,
3333
entryPoints: ["./src/sync.js"],
34-
outdir: "./dist/",
34+
outdir: "./dist/dirsync/",
3535
})
3636
.then(() => console.log("GSuite sync lambda build completed successfully!"))
3737
.catch((error) => {

package.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,18 @@
1010
"build": "tsc && node build.js",
1111
"lint": "prettier --check *.ts **/*.ts",
1212
"prettier:write": "prettier --write *.ts **/*.ts",
13-
"prepare": "husky"
13+
"prepare": "husky",
14+
"test": "vitest --coverage --config ./vitest.config.ts"
1415
},
1516
"devDependencies": {
1617
"@tsconfig/node22": "^22.0.2",
1718
"@types/aws-lambda": "^8.10.138",
1819
"@types/node": "^24.3.0",
20+
"@vitest/coverage-istanbul": "3.2.4",
1921
"esbuild": "^0.25.3",
2022
"husky": "^9.1.7",
21-
"typescript": "^5.9.2"
23+
"typescript": "^5.9.2",
24+
"vitest": "^3.2.4"
2225
},
2326
"dependencies": {
2427
"@aws-sdk/client-secrets-manager": "^3.895.0",

terraform/modules/dirsync/main.tf

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
data "archive_file" "lambda_code" {
22
type = "zip"
3-
source_dir = "${path.module}/../../../dist"
3+
source_dir = "${path.module}/../../../dist/dirsync"
44
output_path = "${path.module}/../../../dist/dirsync.zip"
55
}
66
locals {

test/config.test.ts

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
import { describe, it, expect, beforeEach, vi } from "vitest";
2+
import { getConfig, getSecrets } from "../src/config";
3+
import {
4+
SecretsManagerClient,
5+
GetSecretValueCommand,
6+
} from "@aws-sdk/client-secrets-manager";
7+
8+
vi.mock("@aws-sdk/client-secrets-manager");
9+
10+
describe("config", () => {
11+
beforeEach(() => {
12+
vi.clearAllMocks();
13+
process.env.RunEnvironment = "dev";
14+
});
15+
16+
describe("getSecrets", () => {
17+
it("should return parsed secrets from Secrets Manager", async () => {
18+
const mockSecrets = {
19+
entraTenantId: "tenant-123",
20+
entraClientId: "client-456",
21+
entraClientCertificate: "cert-base64",
22+
googleDelegatedUser: "[email protected]",
23+
googleServiceAccountJson: '{"type":"service_account"}',
24+
deleteRemovedContacts: true,
25+
};
26+
27+
const mockSend = vi.fn().mockResolvedValue({
28+
SecretString: JSON.stringify(mockSecrets),
29+
});
30+
31+
vi.mocked(SecretsManagerClient).mockImplementation(
32+
() =>
33+
({
34+
send: mockSend,
35+
}) as any,
36+
);
37+
38+
const result = await getSecrets();
39+
40+
expect(result).toEqual(mockSecrets);
41+
expect(mockSend).toHaveBeenCalledWith(expect.any(GetSecretValueCommand));
42+
});
43+
44+
it("should return null if SecretString is empty", async () => {
45+
const mockSend = vi.fn().mockResolvedValue({});
46+
47+
vi.mocked(SecretsManagerClient).mockImplementation(
48+
() =>
49+
({
50+
send: mockSend,
51+
}) as any,
52+
);
53+
54+
const result = await getSecrets();
55+
56+
expect(result).toBeNull();
57+
});
58+
59+
it("should return null if JSON parsing fails", async () => {
60+
const mockSend = vi.fn().mockResolvedValue({
61+
SecretString: "invalid-json",
62+
});
63+
64+
vi.mocked(SecretsManagerClient).mockImplementation(
65+
() =>
66+
({
67+
send: mockSend,
68+
}) as any,
69+
);
70+
71+
const result = await getSecrets();
72+
73+
expect(result).toBeNull();
74+
});
75+
});
76+
77+
describe("getConfig", () => {
78+
it("should return valid configuration", async () => {
79+
const mockSecrets = {
80+
entraTenantId: "tenant-123",
81+
entraClientId: "client-456",
82+
entraClientCertificate: "cert-base64",
83+
googleDelegatedUser: "[email protected]",
84+
googleServiceAccountJson: '{"type":"service_account"}',
85+
deleteRemovedContacts: true,
86+
};
87+
88+
const mockSend = vi.fn().mockResolvedValue({
89+
SecretString: JSON.stringify(mockSecrets),
90+
});
91+
92+
vi.mocked(SecretsManagerClient).mockImplementation(
93+
() =>
94+
({
95+
send: mockSend,
96+
}) as any,
97+
);
98+
99+
const result = await getConfig();
100+
101+
expect(result).toEqual({
102+
...mockSecrets,
103+
environment: "dev",
104+
});
105+
});
106+
107+
it("should throw error if secrets cannot be loaded", async () => {
108+
const mockSend = vi.fn().mockResolvedValue({});
109+
110+
vi.mocked(SecretsManagerClient).mockImplementation(
111+
() =>
112+
({
113+
send: mockSend,
114+
}) as any,
115+
);
116+
117+
await expect(getConfig()).rejects.toThrow("Failed to load configuration");
118+
});
119+
120+
it("should validate environment is dev or prod", async () => {
121+
process.env.RunEnvironment = "invalid";
122+
123+
const mockSecrets = {
124+
entraTenantId: "tenant-123",
125+
entraClientId: "client-456",
126+
entraClientCertificate: "cert-base64",
127+
googleDelegatedUser: "[email protected]",
128+
googleServiceAccountJson: '{"type":"service_account"}',
129+
deleteRemovedContacts: true,
130+
};
131+
132+
const mockSend = vi.fn().mockResolvedValue({
133+
SecretString: JSON.stringify(mockSecrets),
134+
});
135+
136+
vi.mocked(SecretsManagerClient).mockImplementation(
137+
() =>
138+
({
139+
send: mockSend,
140+
}) as any,
141+
);
142+
143+
await expect(getConfig()).rejects.toThrow();
144+
});
145+
146+
it("should validate required fields", async () => {
147+
const mockSecrets = {
148+
entraTenantId: "",
149+
entraClientId: "client-456",
150+
entraClientCertificate: "cert-base64",
151+
googleDelegatedUser: "[email protected]",
152+
googleServiceAccountJson: '{"type":"service_account"}',
153+
};
154+
155+
const mockSend = vi.fn().mockResolvedValue({
156+
SecretString: JSON.stringify(mockSecrets),
157+
});
158+
159+
vi.mocked(SecretsManagerClient).mockImplementation(
160+
() =>
161+
({
162+
send: mockSend,
163+
}) as any,
164+
);
165+
166+
await expect(getConfig()).rejects.toThrow();
167+
});
168+
169+
it("should default deleteRemovedContacts to true", async () => {
170+
const mockSecrets = {
171+
entraTenantId: "tenant-123",
172+
entraClientId: "client-456",
173+
entraClientCertificate: "cert-base64",
174+
googleDelegatedUser: "[email protected]",
175+
googleServiceAccountJson: '{"type":"service_account"}',
176+
};
177+
178+
const mockSend = vi.fn().mockResolvedValue({
179+
SecretString: JSON.stringify(mockSecrets),
180+
});
181+
182+
vi.mocked(SecretsManagerClient).mockImplementation(
183+
() =>
184+
({
185+
send: mockSend,
186+
}) as any,
187+
);
188+
189+
const result = await getConfig();
190+
191+
expect(result.deleteRemovedContacts).toBe(true);
192+
});
193+
});
194+
});

0 commit comments

Comments
 (0)