@freshworks/shiftleft-tools 1.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +351 -0
- package/bin/shiftleft.js +95 -0
- package/package.json +57 -0
- package/src/commands/doctor.js +208 -0
- package/src/commands/init-postman.js +298 -0
- package/src/commands/init-rules.js +78 -0
- package/src/commands/link.js +172 -0
- package/src/commands/protect.js +61 -0
- package/src/commands/run-tests.js +182 -0
- package/src/commands/setup-pipeline.js +209 -0
- package/src/commands/update.js +203 -0
- package/src/index.js +4 -0
- package/src/utils/copy-tree.js +98 -0
- package/src/utils/gitignore.js +26 -0
- package/src/utils/logger.js +9 -0
- package/src/utils/manifest.js +145 -0
- package/src/utils/stack.js +80 -0
- package/src/utils/template.js +135 -0
- package/templates/AGENTS.md +109 -0
- package/templates/CLAUDE.md +3 -0
- package/templates/jenkins/Jenkinsfile-java.groovy +432 -0
- package/templates/jenkins/Jenkinsfile-node.groovy +450 -0
- package/templates/postman/.husky/pre-commit +19 -0
- package/templates/postman/.prettierrc.json +5 -0
- package/templates/postman/README.md.ejs +147 -0
- package/templates/postman/collections/01-core.json.ejs +91 -0
- package/templates/postman/config/local.json.ejs +12 -0
- package/templates/postman/config/staging.json.ejs +26 -0
- package/templates/postman/environments/local.postman_environment.json.ejs +31 -0
- package/templates/postman/environments/staging.postman_environment.json.ejs +31 -0
- package/templates/postman/gitignore +16 -0
- package/templates/postman/npmrc +31 -0
- package/templates/postman/package.json.ejs +66 -0
- package/templates/postman/run-all-shim.sh +16 -0
- package/templates/postman/scripts/auth/generate-jwt.sh +113 -0
- package/templates/postman/scripts/auth/get-issuer-secret.sh +140 -0
- package/templates/postman/scripts/infra/start-mocks.sh +138 -0
- package/templates/postman/scripts/infra/stop-mocks.sh +43 -0
- package/templates/postman/scripts/lib/api_coverage.py +1122 -0
- package/templates/postman/scripts/lib/cleanup-reports.sh +101 -0
- package/templates/postman/scripts/lib/cleanup-stryker.sh +44 -0
- package/templates/postman/scripts/lib/report_combined.py +527 -0
- package/templates/postman/scripts/lib/report_consolidated.py +363 -0
- package/templates/postman/scripts/lib/report_generator.py +121 -0
- package/templates/postman/scripts/lib/report_migration.py +156 -0
- package/templates/postman/scripts/lib/report_mutation.py +110 -0
- package/templates/postman/scripts/lib/report_unit.py +353 -0
- package/templates/postman/scripts/lib/report_utils.py +973 -0
- package/templates/postman/scripts/report-generators/generate-consolidated-report.sh +445 -0
- package/templates/postman/scripts/report-generators/java-api-coverage-matrix.sh +257 -0
- package/templates/postman/scripts/report-generators/mutation-report.sh +672 -0
- package/templates/postman/scripts/report-generators/node-api-coverage-matrix.sh +167 -0
- package/templates/postman/scripts/report-generators/stage-report-artifacts.sh +27 -0
- package/templates/postman/scripts/run-all.sh +452 -0
- package/templates/postman/scripts/runners/run-mutation-tests.sh +113 -0
- package/templates/postman/scripts/runners/run-tests-local.sh +936 -0
- package/templates/postman/scripts/runners/run-tests-staging.sh +741 -0
- package/templates/postman-node/README.md.ejs +26 -0
- package/templates/postman-node/collections/crud/01-bootstrap.json.ejs +34 -0
- package/templates/postman-node/config/local.json.ejs +46 -0
- package/templates/postman-node/config/staging.json.ejs +31 -0
- package/templates/postman-node/local.test.env.ejs +3 -0
- package/templates/postman-node/mocks/external.js +14 -0
- package/templates/postman-node/package.json.ejs +39 -0
- package/templates/postman-node/requirements.txt +1 -0
- package/templates/postman-node/scripts/database/cleanup-mysql.sh +12 -0
- package/templates/postman-node/scripts/database/run-migrations.js +29 -0
- package/templates/postman-node/scripts/database/start-mysql.sh +34 -0
- package/templates/postman-node/scripts/database/wait-for-mysql.sh +36 -0
- package/templates/postman-node/scripts/lib/api_coverage_node.py +1137 -0
- package/templates/postman-node/scripts/lib/fetch-jwt.sh +86 -0
- package/templates/postman-node/scripts/lib/run-newman.sh +104 -0
- package/templates/postman-node/scripts/lib/setup-database.sh +55 -0
- package/templates/postman-node/scripts/lib/start-app.sh +48 -0
- package/templates/postman-node/scripts/lib/utils.sh +114 -0
- package/templates/postman-node/scripts/report-generators/stage-report-artifacts.sh +26 -0
- package/templates/postman-node/scripts/run-all.sh +303 -0
- package/templates/postman-node/scripts/runners/run-tests.sh +123 -0
- package/templates/postman-node/scripts/setup-mocks.js.ejs +29 -0
- package/templates/postman-node/stryker.config.js.ejs +51 -0
- package/templates/rules/local-test-setup.mdc +420 -0
- package/templates/rules/testing-node.mdc +66 -0
- package/templates/rules/testing.mdc +248 -0
- package/templates/skills/_shared/postman-standards.md +380 -0
- package/templates/skills/enhance-test-pipeline/SKILL-java.md +483 -0
- package/templates/skills/enhance-test-pipeline/SKILL-node.md +431 -0
- package/templates/skills/enhance-test-pipeline/SKILL.md +9 -0
- package/templates/skills/review-test-suite/SKILL-java.md +137 -0
- package/templates/skills/review-test-suite/SKILL-node.md +78 -0
- package/templates/skills/review-test-suite/SKILL.md +9 -0
- package/templates/skills/run-test-suite/SKILL-java.md +186 -0
- package/templates/skills/run-test-suite/SKILL-node.md +191 -0
- package/templates/skills/run-test-suite/SKILL.md +9 -0
- package/templates/skills/setup-api-tests/SKILL-java.md +1094 -0
- package/templates/skills/setup-api-tests/SKILL-node.md +141 -0
- package/templates/skills/setup-api-tests/SKILL.md +9 -0
- package/templates/skills/setup-mutation-tests/SKILL-java.md +303 -0
- package/templates/skills/setup-mutation-tests/SKILL-node.md +408 -0
- package/templates/skills/setup-mutation-tests/SKILL.md +9 -0
- package/templates/skills/setup-test-pipeline/SKILL-java.md +454 -0
- package/templates/skills/setup-test-pipeline/SKILL-node.md +318 -0
- package/templates/skills/setup-test-pipeline/SKILL.md +9 -0
- package/templates/skills/write-api-tests/SKILL-java.md +115 -0
- package/templates/skills/write-api-tests/SKILL-node.md +83 -0
- package/templates/skills/write-api-tests/SKILL.md +9 -0
- package/templates/stryker.config.js +50 -0
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Test writing rules - MANDATORY when editing test files
|
|
3
|
+
globs:
|
|
4
|
+
- "**/*Test.java"
|
|
5
|
+
- "**/*IntegrationTest.java"
|
|
6
|
+
- "**/src/test/**/*.java"
|
|
7
|
+
- "**/src/integration-test/**/*.java"
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Test Writing Rules - MANDATORY
|
|
11
|
+
|
|
12
|
+
All tests MUST survive PIT mutation testing. Weak assertions WILL fail.
|
|
13
|
+
|
|
14
|
+
## CRITICAL: Mutation Testing (PIT)
|
|
15
|
+
|
|
16
|
+
- Target mutation score: **80%+**
|
|
17
|
+
- Run mutation tests: `./postman/scripts/report-generators/mutation-report.sh`
|
|
18
|
+
- Weak assertions allow mutants to survive = test is worthless
|
|
19
|
+
|
|
20
|
+
## Assertion Strength - CRITICAL
|
|
21
|
+
|
|
22
|
+
NEVER write weak assertions. They prove nothing and mutants will survive.
|
|
23
|
+
|
|
24
|
+
### WRONG - Weak Assertions (DO NOT USE):
|
|
25
|
+
|
|
26
|
+
```java
|
|
27
|
+
assertNotNull(result); // WRONG - proves nothing
|
|
28
|
+
assertTrue(result.isPresent()); // WRONG - no value check
|
|
29
|
+
assertThat(list).isNotEmpty(); // WRONG - no content check
|
|
30
|
+
assertThat(count).isGreaterThan(0); // WRONG - boundary not tested
|
|
31
|
+
assertThat(result).isNotNull(); // WRONG - still proves nothing
|
|
32
|
+
verify(repository).save(any()); // WRONG - content not verified
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### CORRECT - Strong Assertions (USE THESE):
|
|
36
|
+
|
|
37
|
+
```java
|
|
38
|
+
assertThat(result.getId()).isEqualTo(expectedId); // Specific value
|
|
39
|
+
assertThat(result.getName()).isEqualTo("Expected Name"); // Exact match
|
|
40
|
+
assertThat(result.getStatus()).isEqualTo(Status.ACTIVE); // Enum value
|
|
41
|
+
assertThat(list).containsExactly(item1, item2); // Content verified
|
|
42
|
+
assertThat(list).hasSize(3); // Combined with content check
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Test All Output Fields
|
|
46
|
+
|
|
47
|
+
If a method returns an object with 5 fields, you MUST assert on ALL 5 fields.
|
|
48
|
+
|
|
49
|
+
### WRONG:
|
|
50
|
+
|
|
51
|
+
```java
|
|
52
|
+
AppDTO result = service.getApp("123");
|
|
53
|
+
assertThat(result.getId()).isEqualTo("123"); // WRONG - only checking 1 field
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### CORRECT:
|
|
57
|
+
|
|
58
|
+
```java
|
|
59
|
+
AppDTO result = service.getApp("123");
|
|
60
|
+
assertThat(result.getId()).isEqualTo("123");
|
|
61
|
+
assertThat(result.getName()).isEqualTo("Test App");
|
|
62
|
+
assertThat(result.getType()).isEqualTo(AppType.PLATFORM);
|
|
63
|
+
assertThat(result.getStatus()).isEqualTo(Status.ACTIVE);
|
|
64
|
+
assertThat(result.getCreatedAt()).isNotNull(); // OK for timestamps
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Verify Saved Arguments with ArgumentCaptor
|
|
68
|
+
|
|
69
|
+
### WRONG:
|
|
70
|
+
|
|
71
|
+
```java
|
|
72
|
+
verify(repository).save(any()); // Mutant survives - content not verified
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### CORRECT:
|
|
76
|
+
|
|
77
|
+
```java
|
|
78
|
+
ArgumentCaptor<App> captor = ArgumentCaptor.forClass(App.class);
|
|
79
|
+
verify(repository).save(captor.capture());
|
|
80
|
+
App saved = captor.getValue();
|
|
81
|
+
assertThat(saved.getName()).isEqualTo("Expected Name");
|
|
82
|
+
assertThat(saved.getType()).isEqualTo(AppType.CUSTOM);
|
|
83
|
+
assertThat(saved.getStatus()).isEqualTo(Status.PENDING);
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Exception Messages MUST Be Verified
|
|
87
|
+
|
|
88
|
+
### WRONG:
|
|
89
|
+
|
|
90
|
+
```java
|
|
91
|
+
assertThrows(ResourceNotFoundException.class, () -> service.getApp("invalid"));
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### CORRECT:
|
|
95
|
+
|
|
96
|
+
```java
|
|
97
|
+
var ex = assertThrows(ResourceNotFoundException.class, () -> service.getApp("invalid"));
|
|
98
|
+
assertThat(ex.getMessage()).isEqualTo("App not found with id: invalid");
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Use Parameterized Tests for Combinations
|
|
102
|
+
|
|
103
|
+
For methods with multiple inputs, you MUST use `@ParameterizedTest`:
|
|
104
|
+
|
|
105
|
+
```java
|
|
106
|
+
@ParameterizedTest
|
|
107
|
+
@EnumSource(AppType.class)
|
|
108
|
+
void getApps_shouldFilterByAllAppTypes(AppType type) {
|
|
109
|
+
// Test each enum value
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
@ParameterizedTest
|
|
113
|
+
@MethodSource("invalidInputs")
|
|
114
|
+
void create_shouldRejectInvalidInputs(String name, String expectedError) {
|
|
115
|
+
var ex = assertThrows(BadRequestException.class, () -> service.create(name));
|
|
116
|
+
assertThat(ex.getMessage()).contains(expectedError);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
static Stream<Arguments> invalidInputs() {
|
|
120
|
+
return Stream.of(
|
|
121
|
+
Arguments.of(null, "Name is required"),
|
|
122
|
+
Arguments.of("", "Name is required"),
|
|
123
|
+
Arguments.of("a".repeat(256), "Name must not exceed 255")
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Test Naming Convention
|
|
129
|
+
|
|
130
|
+
Use: `methodName_whenCondition_shouldExpectedResult`
|
|
131
|
+
|
|
132
|
+
```java
|
|
133
|
+
@Test
|
|
134
|
+
void getAppById_whenAppExists_shouldReturnAppDetails() { }
|
|
135
|
+
|
|
136
|
+
@Test
|
|
137
|
+
void getAppById_whenAppNotFound_shouldThrowResourceNotFoundException() { }
|
|
138
|
+
|
|
139
|
+
@Test
|
|
140
|
+
void createApp_whenValidRequest_shouldReturnCreatedApp() { }
|
|
141
|
+
|
|
142
|
+
@Test
|
|
143
|
+
void createApp_whenDuplicateName_shouldThrowConflictException() { }
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
## Unit Test Structure
|
|
147
|
+
|
|
148
|
+
```java
|
|
149
|
+
@ExtendWith(MockitoExtension.class)
|
|
150
|
+
class AppServiceTest {
|
|
151
|
+
@Mock
|
|
152
|
+
private AppRepository appRepository;
|
|
153
|
+
|
|
154
|
+
@Mock
|
|
155
|
+
private AppMapper appMapper;
|
|
156
|
+
|
|
157
|
+
@InjectMocks
|
|
158
|
+
private AppService appService;
|
|
159
|
+
|
|
160
|
+
@Nested
|
|
161
|
+
class GetAppById {
|
|
162
|
+
@Test
|
|
163
|
+
void whenAppExists_shouldReturnAppDetails() {
|
|
164
|
+
// Given
|
|
165
|
+
String appId = "app-123";
|
|
166
|
+
App app = App.builder().externalId(appId).name("Test").build();
|
|
167
|
+
AppDetailsDTO expected = AppDetailsDTO.builder().id(appId).name("Test").build();
|
|
168
|
+
|
|
169
|
+
when(appRepository.findByExternalId(appId)).thenReturn(Optional.of(app));
|
|
170
|
+
when(appMapper.toDetailsDTO(app)).thenReturn(expected);
|
|
171
|
+
|
|
172
|
+
// When
|
|
173
|
+
AppDetailsDTO result = appService.getAppById(appId);
|
|
174
|
+
|
|
175
|
+
// Then
|
|
176
|
+
assertThat(result.getId()).isEqualTo(appId);
|
|
177
|
+
assertThat(result.getName()).isEqualTo("Test");
|
|
178
|
+
verify(appRepository).findByExternalId(appId);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
@Test
|
|
182
|
+
void whenAppNotFound_shouldThrowException() {
|
|
183
|
+
// Given
|
|
184
|
+
when(appRepository.findByExternalId("invalid")).thenReturn(Optional.empty());
|
|
185
|
+
|
|
186
|
+
// When/Then
|
|
187
|
+
var ex = assertThrows(ResourceNotFoundException.class,
|
|
188
|
+
() -> appService.getAppById("invalid"));
|
|
189
|
+
assertThat(ex.getMessage()).isEqualTo("App not found with id: invalid");
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
## Integration Test Structure
|
|
196
|
+
|
|
197
|
+
```java
|
|
198
|
+
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
|
|
199
|
+
@AutoConfigureMockMvc
|
|
200
|
+
@ActiveProfiles("integration")
|
|
201
|
+
@Sql(scripts = "/data-sql/apps.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
|
|
202
|
+
@Sql(scripts = "/data-sql/cleanup.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
|
|
203
|
+
class AppControllerIntegrationTest {
|
|
204
|
+
@Autowired
|
|
205
|
+
private MockMvc mockMvc;
|
|
206
|
+
|
|
207
|
+
@Autowired
|
|
208
|
+
private JdbcTemplate jdbcTemplate;
|
|
209
|
+
|
|
210
|
+
@MockBean
|
|
211
|
+
private FreshIDApiClient freshIDApiClient; // Mock external services only
|
|
212
|
+
|
|
213
|
+
@Test
|
|
214
|
+
void getApp_whenExists_shouldReturn200WithDetails() throws Exception {
|
|
215
|
+
mockMvc.perform(get("/api/v1/apps/{id}", "app-1")
|
|
216
|
+
.header("Authorization", "Bearer test-token"))
|
|
217
|
+
.andExpect(status().isOk())
|
|
218
|
+
.andExpect(jsonPath("$.id").value("app-1"))
|
|
219
|
+
.andExpect(jsonPath("$.name").value("Test App"))
|
|
220
|
+
.andExpect(jsonPath("$.type").value("PLATFORM"));
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
@Test
|
|
224
|
+
void getApp_whenNotFound_shouldReturn404() throws Exception {
|
|
225
|
+
mockMvc.perform(get("/api/v1/apps/{id}", "nonexistent")
|
|
226
|
+
.header("Authorization", "Bearer test-token"))
|
|
227
|
+
.andExpect(status().isNotFound())
|
|
228
|
+
.andExpect(jsonPath("$.detail").value("App not found with id: nonexistent"));
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
## Running Tests
|
|
234
|
+
|
|
235
|
+
| Command | What it runs |
|
|
236
|
+
|---------|--------------|
|
|
237
|
+
| `mvn test` | Unit tests only |
|
|
238
|
+
| `./postman/scripts/report-generators/mutation-report.sh` | PIT mutation testing |
|
|
239
|
+
| `cd postman/scripts && ./runners/run-tests-local.sh` | Postman API tests (local) |
|
|
240
|
+
| `cd postman/scripts && ./run-all.sh` | Full test suite |
|
|
241
|
+
| `./postman/scripts/report-generators/java-api-coverage-matrix.sh` | API coverage report |
|
|
242
|
+
|
|
243
|
+
## Before Merge Checklist
|
|
244
|
+
|
|
245
|
+
1. `mvn test` - all unit tests pass
|
|
246
|
+
2. `./postman/scripts/runners/run-tests-local.sh` - all Postman tests pass
|
|
247
|
+
3. `./postman/scripts/report-generators/java-api-coverage-matrix.sh` - 100% 2xx coverage
|
|
248
|
+
4. `./postman/scripts/report-generators/mutation-report.sh` - 80%+ mutation score
|
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
# Postman/Newman Standards
|
|
2
|
+
|
|
3
|
+
Language-agnostic standards for writing and organizing Postman integration tests. Referenced by all language-specific skill files.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Library scripts are staged from the package — never copied or committed
|
|
8
|
+
|
|
9
|
+
The pipeline's library scripts (`postman/scripts/lib/`, `runners/`,
|
|
10
|
+
`report-generators/`, `auth/`, `infra/`, `database/`) are **not** vendored into
|
|
11
|
+
the repo. They live in the installed `shiftleft-tools` package and are pulled in
|
|
12
|
+
on demand:
|
|
13
|
+
|
|
14
|
+
- `shiftleft test` (and the committed `postman/scripts/run-all.sh` shim) stage the
|
|
15
|
+
current scripts automatically before every run.
|
|
16
|
+
- `shiftleft stage-scripts` stages them on their own — run this first if you need
|
|
17
|
+
to invoke an individual staged script directly (e.g.
|
|
18
|
+
`./postman/scripts/report-generators/...`).
|
|
19
|
+
|
|
20
|
+
Staged scripts are gitignored and refreshed each run, so they always match the
|
|
21
|
+
installed package. **Do not copy scripts from a templates directory, write them
|
|
22
|
+
from scratch, or commit them.** Only `run-all.sh` (a thin shim) and repo-owned
|
|
23
|
+
files (collections, config, environments, Node's `setup-mocks.js`, Java's
|
|
24
|
+
`wiremock/mappings/`) are committed.
|
|
25
|
+
|
|
26
|
+
**Exception — a customized library script.** If a repo must customize a library
|
|
27
|
+
script (e.g. a service-specific `runners/run-tests-local.sh`), mark it protected
|
|
28
|
+
so staging won't overwrite it: `shiftleft protect runners/run-tests-local.sh`.
|
|
29
|
+
Protected paths are recorded in `.shiftleft.json` and must stay committed.
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## Collection Organization
|
|
34
|
+
|
|
35
|
+
Collections live in `postman/collections/` with numbered naming for execution order.
|
|
36
|
+
|
|
37
|
+
**Naming convention**: `XX-description.json`
|
|
38
|
+
|
|
39
|
+
**Example structure** (explore actual folder for this project's collections):
|
|
40
|
+
```
|
|
41
|
+
postman/collections/
|
|
42
|
+
├── 01-bootstrap.json # Creates prerequisite test data, sets env vars
|
|
43
|
+
├── 02-core-crud.json # Core CRUD operations
|
|
44
|
+
├── 03-authorization.json # Auth tests (may be STAGING ONLY)
|
|
45
|
+
├── 04-query-params.json # Sorting, filtering, pagination
|
|
46
|
+
├── 05-validation.json # Invalid input, 400 errors
|
|
47
|
+
└── 99-cleanup.json # Deletes all test data, always runs last
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
**Common patterns**:
|
|
51
|
+
- Authorization tests often skip in local (no API gateway)
|
|
52
|
+
- Internal/cache endpoints often skip in staging (not exposed)
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## Writing Test Assertions
|
|
57
|
+
|
|
58
|
+
### MANDATORY: One Status Code Per Test
|
|
59
|
+
|
|
60
|
+
Each test request MUST assert exactly ONE HTTP status code:
|
|
61
|
+
|
|
62
|
+
```javascript
|
|
63
|
+
// CORRECT - Single status code
|
|
64
|
+
pm.test("Status code is 200", function () {
|
|
65
|
+
pm.response.to.have.status(200);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// WRONG - Multiple status codes
|
|
69
|
+
pm.test("Status is valid", function () {
|
|
70
|
+
pm.expect(pm.response.code).to.be.oneOf([200, 201]); // NEVER do this
|
|
71
|
+
});
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### MANDATORY: Strong Value Assertions
|
|
75
|
+
|
|
76
|
+
```javascript
|
|
77
|
+
// WRONG - Weak assertions
|
|
78
|
+
pm.test("Response has data", function () {
|
|
79
|
+
pm.expect(pm.response.json()).to.not.be.null; // Proves nothing
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// CORRECT - Strong assertions
|
|
83
|
+
pm.test("Response has correct data", function () {
|
|
84
|
+
const json = pm.response.json();
|
|
85
|
+
pm.expect(json.id).to.eql("app-123");
|
|
86
|
+
pm.expect(json.name).to.eql("Expected Name");
|
|
87
|
+
pm.expect(json.type).to.eql("PLATFORM");
|
|
88
|
+
});
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### FORBIDDEN: 5xx Status Code Assertions
|
|
92
|
+
|
|
93
|
+
NEVER assert on 5xx status codes - they indicate bugs, not expected behavior:
|
|
94
|
+
|
|
95
|
+
```javascript
|
|
96
|
+
// FORBIDDEN - Never do this
|
|
97
|
+
pm.test("Status code is 500", function () {
|
|
98
|
+
pm.response.to.have.status(500);
|
|
99
|
+
});
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## Environment-Specific Skip Pattern
|
|
105
|
+
|
|
106
|
+
Some tests behave differently between local and staging. Use this pattern:
|
|
107
|
+
|
|
108
|
+
### Skip in Local (Staging-Only Test)
|
|
109
|
+
|
|
110
|
+
```javascript
|
|
111
|
+
// Pre-request Script
|
|
112
|
+
if (pm.environment.get('environment') === 'local') {
|
|
113
|
+
console.log('[SKIP] Reason: JWT validation happens at gateway, local has no gateway | Env: local');
|
|
114
|
+
pm.execution.skipRequest();
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### Skip in Staging (Local-Only Test)
|
|
119
|
+
|
|
120
|
+
```javascript
|
|
121
|
+
// Pre-request Script
|
|
122
|
+
if (pm.environment.get('environment') !== 'local') {
|
|
123
|
+
console.log('[SKIP] Reason: Internal endpoint not exposed in staging | Env: staging');
|
|
124
|
+
pm.execution.skipRequest();
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### Skip Log Format (MANDATORY)
|
|
129
|
+
|
|
130
|
+
```
|
|
131
|
+
[SKIP] Reason: <why this test is skipped> | Env: <local|staging>
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### Naming Convention for Environment-Specific Tests
|
|
135
|
+
|
|
136
|
+
Append `(local)` or `(staging)` to test names:
|
|
137
|
+
|
|
138
|
+
```
|
|
139
|
+
"Create - Invalid Token (staging)" // Expects 401, skipped in local
|
|
140
|
+
"Create - Invalid Token (local)" // Expects 400, skipped in staging
|
|
141
|
+
"PATCH - 404 non-existent (staging)" // Expects 404, skipped in local
|
|
142
|
+
"PATCH - 403 non-existent (local)" // Expects 403, skipped in staging
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
## Why Tests Differ by Environment
|
|
148
|
+
|
|
149
|
+
| Scenario | Local Behavior | Staging Behavior |
|
|
150
|
+
|----------|---------------|------------------|
|
|
151
|
+
| Invalid JWT token | 400 Bad Request | 401 Unauthorized (API Gateway) |
|
|
152
|
+
| Non-existent resource with bad auth | 403 Forbidden | 404 Not Found |
|
|
153
|
+
| Internal endpoints | Works | Not exposed |
|
|
154
|
+
| Authorization checks | @PreAuthorize first | Resource lookup first |
|
|
155
|
+
|
|
156
|
+
---
|
|
157
|
+
|
|
158
|
+
## HTTP Status Coverage Requirements
|
|
159
|
+
|
|
160
|
+
Every endpoint MUST have tests for:
|
|
161
|
+
|
|
162
|
+
| Status | When | Priority |
|
|
163
|
+
|--------|------|----------|
|
|
164
|
+
| 200 OK | GET/PUT/PATCH success | High |
|
|
165
|
+
| 201 Created | POST success | High |
|
|
166
|
+
| 400 Bad Request | Invalid input/validation | High |
|
|
167
|
+
| 401 Unauthorized | Missing/invalid auth | High |
|
|
168
|
+
| 404 Not Found | Resource not found | High |
|
|
169
|
+
| 204 No Content | DELETE success | Medium |
|
|
170
|
+
| 409 Conflict | Duplicate/concurrent update | Medium |
|
|
171
|
+
|
|
172
|
+
---
|
|
173
|
+
|
|
174
|
+
## Query Parameter Coverage - MANDATORY
|
|
175
|
+
|
|
176
|
+
For endpoints with query parameters, you MUST test each parameter with multiple values.
|
|
177
|
+
|
|
178
|
+
### What to Test
|
|
179
|
+
|
|
180
|
+
| Parameter Type | Required Tests |
|
|
181
|
+
|---------------|----------------|
|
|
182
|
+
| **Filter params** (type, status) | Each valid enum/value |
|
|
183
|
+
| **Sort params** (sort, orderBy) | Ascending AND descending |
|
|
184
|
+
| **Pagination** (page, size) | First page, middle page, edge cases |
|
|
185
|
+
| **Search** (query, q) | Valid search, empty results |
|
|
186
|
+
|
|
187
|
+
### Example: GET /apps with Query Params
|
|
188
|
+
|
|
189
|
+
```javascript
|
|
190
|
+
// Test each filter value
|
|
191
|
+
GET /apps?type=PLATFORM
|
|
192
|
+
GET /apps?type=CUSTOM
|
|
193
|
+
GET /apps?type=NATIVE
|
|
194
|
+
|
|
195
|
+
// Test sorting - BOTH directions
|
|
196
|
+
GET /apps?sort=name,asc
|
|
197
|
+
GET /apps?sort=name,desc
|
|
198
|
+
GET /apps?sort=createdAt,desc
|
|
199
|
+
|
|
200
|
+
// Test pagination
|
|
201
|
+
GET /apps?page=0&size=10
|
|
202
|
+
GET /apps?page=1&size=5
|
|
203
|
+
GET /apps?size=100 // Max size
|
|
204
|
+
|
|
205
|
+
// Test combinations
|
|
206
|
+
GET /apps?type=PLATFORM&sort=name,asc&page=0&size=20
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
### Assertions for Query Params
|
|
210
|
+
|
|
211
|
+
```javascript
|
|
212
|
+
// For filter tests - verify filtered results
|
|
213
|
+
pm.test("All results match filter", function () {
|
|
214
|
+
const json = pm.response.json();
|
|
215
|
+
json.content.forEach(item => {
|
|
216
|
+
pm.expect(item.type).to.eql("PLATFORM");
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
// For sort tests - verify order
|
|
221
|
+
pm.test("Results sorted by name ascending", function () {
|
|
222
|
+
const json = pm.response.json();
|
|
223
|
+
const names = json.content.map(item => item.name);
|
|
224
|
+
const sorted = [...names].sort();
|
|
225
|
+
pm.expect(names).to.eql(sorted);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
// For pagination - verify page metadata
|
|
229
|
+
pm.test("Pagination metadata correct", function () {
|
|
230
|
+
const json = pm.response.json();
|
|
231
|
+
pm.expect(json.page).to.eql(0);
|
|
232
|
+
pm.expect(json.size).to.eql(10);
|
|
233
|
+
pm.expect(json.totalElements).to.be.a('number');
|
|
234
|
+
});
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
---
|
|
238
|
+
|
|
239
|
+
## Include Parameter Coverage - MANDATORY
|
|
240
|
+
|
|
241
|
+
For endpoints with `include` parameter, test ALL expected values individually AND in combination.
|
|
242
|
+
|
|
243
|
+
### Expected Include Values
|
|
244
|
+
|
|
245
|
+
| Endpoint Type | Expected Values |
|
|
246
|
+
|--------------|-----------------|
|
|
247
|
+
| List endpoints (`/apps`, `/apps/{id}`) | `configs`, `oauth_configs_list`, `secure_iparams` |
|
|
248
|
+
| Configuration endpoints | `secure_iparams`, `oauth_iparams`, `extension_type` |
|
|
249
|
+
|
|
250
|
+
### Example: Testing Include Parameter
|
|
251
|
+
|
|
252
|
+
```javascript
|
|
253
|
+
// Test each include value INDIVIDUALLY
|
|
254
|
+
GET /apps/{id}?include=configs
|
|
255
|
+
GET /apps/{id}?include=oauth_configs_list
|
|
256
|
+
GET /apps/{id}?include=secure_iparams
|
|
257
|
+
|
|
258
|
+
// Test combinations
|
|
259
|
+
GET /apps/{id}?include=configs,oauth_configs_list
|
|
260
|
+
GET /apps/{id}?include=configs,oauth_configs_list,secure_iparams
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
### Assertions for Include Tests
|
|
264
|
+
|
|
265
|
+
```javascript
|
|
266
|
+
// Verify included data is present
|
|
267
|
+
pm.test("Response includes configs", function () {
|
|
268
|
+
const json = pm.response.json();
|
|
269
|
+
pm.expect(json).to.have.property('configs');
|
|
270
|
+
pm.expect(json.configs).to.be.an('array');
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
// Verify data is NOT included when not requested
|
|
274
|
+
pm.test("Response excludes oauth_configs when not requested", function () {
|
|
275
|
+
const json = pm.response.json();
|
|
276
|
+
pm.expect(json.oauthConfigs).to.be.undefined;
|
|
277
|
+
});
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
---
|
|
281
|
+
|
|
282
|
+
## Request Body Variations - For POST/PUT/PATCH
|
|
283
|
+
|
|
284
|
+
For write operations, test different body scenarios to ensure validation works correctly.
|
|
285
|
+
|
|
286
|
+
### Required Body Test Cases
|
|
287
|
+
|
|
288
|
+
| Test Case | Purpose | Expected Status |
|
|
289
|
+
|-----------|---------|-----------------|
|
|
290
|
+
| All required fields | Happy path | 201/200 |
|
|
291
|
+
| Required + optional fields | Full payload | 201/200 |
|
|
292
|
+
| Minimal valid body | Edge case | 201/200 |
|
|
293
|
+
| Missing required field | Validation | 400 |
|
|
294
|
+
| Invalid field value | Validation | 400 |
|
|
295
|
+
| Empty body | Validation | 400 |
|
|
296
|
+
| Extra unknown fields | Ignored/rejected | 201 or 400 |
|
|
297
|
+
|
|
298
|
+
### Example: POST /apps Body Variations
|
|
299
|
+
|
|
300
|
+
```javascript
|
|
301
|
+
// Test 1: All required fields
|
|
302
|
+
{
|
|
303
|
+
"name": "Test App",
|
|
304
|
+
"type": "PLATFORM"
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Test 2: With optional fields
|
|
308
|
+
{
|
|
309
|
+
"name": "Test App",
|
|
310
|
+
"type": "PLATFORM",
|
|
311
|
+
"description": "Optional description",
|
|
312
|
+
"tags": ["tag1", "tag2"]
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Test 3: Missing required field (400)
|
|
316
|
+
{
|
|
317
|
+
"type": "PLATFORM"
|
|
318
|
+
// name is missing
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Test 4: Invalid enum value (400)
|
|
322
|
+
{
|
|
323
|
+
"name": "Test App",
|
|
324
|
+
"type": "INVALID_TYPE"
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Test 5: Invalid field format (400)
|
|
328
|
+
{
|
|
329
|
+
"name": "", // Empty string
|
|
330
|
+
"type": "PLATFORM"
|
|
331
|
+
}
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
### Assertions for Body Validation Tests
|
|
335
|
+
|
|
336
|
+
```javascript
|
|
337
|
+
// For 400 errors - verify error details
|
|
338
|
+
pm.test("Error response has details", function () {
|
|
339
|
+
const json = pm.response.json();
|
|
340
|
+
pm.expect(json).to.have.property('detail');
|
|
341
|
+
pm.expect(json.detail).to.include('name'); // Field name in error
|
|
342
|
+
});
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
---
|
|
346
|
+
|
|
347
|
+
## Collection Variables
|
|
348
|
+
|
|
349
|
+
Use variables for dynamic values:
|
|
350
|
+
|
|
351
|
+
- `{{base_url}}` - API base URL
|
|
352
|
+
- `{{auth_token}}` - JWT token
|
|
353
|
+
- `{{app_id}}` - Created app ID (from previous request)
|
|
354
|
+
- `{{version_id}}` - Created version ID
|
|
355
|
+
- `{{tenant_id}}` - Test tenant identifier
|
|
356
|
+
- `{{environment}}` - Current environment (local/staging)
|
|
357
|
+
|
|
358
|
+
## Pre-request Script: Save Values
|
|
359
|
+
|
|
360
|
+
```javascript
|
|
361
|
+
// Save value from previous response for chaining
|
|
362
|
+
pm.collectionVariables.set("app_id", pm.response.json().id);
|
|
363
|
+
|
|
364
|
+
// Generate timestamp
|
|
365
|
+
pm.collectionVariables.set("timestamp", Date.now());
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
---
|
|
369
|
+
|
|
370
|
+
## Quick Checklist Before Submitting Tests
|
|
371
|
+
|
|
372
|
+
- [ ] Each test asserts exactly ONE status code
|
|
373
|
+
- [ ] No `oneOf`, `to.include([...])` for status codes
|
|
374
|
+
- [ ] No 5xx status code assertions
|
|
375
|
+
- [ ] All query parameters have tests with multiple values
|
|
376
|
+
- [ ] All include values tested individually
|
|
377
|
+
- [ ] POST/PUT/PATCH have body variation tests
|
|
378
|
+
- [ ] Strong assertions on response fields (not just `isNotNull`)
|
|
379
|
+
- [ ] Environment skips have `[SKIP] Reason: ... | Env: ...`
|
|
380
|
+
- [ ] Coverage matrix shows 100% 2xx coverage
|