• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

andrzej-woof / stuntman / 8908932552

01 May 2024 11:48AM CUT coverage: 81.27%. Remained the same
8908932552

push

github

andrzej-woof
and again

611 of 668 branches covered (91.47%)

Branch coverage included in aggregate %.

2218 of 2813 relevant lines covered (78.85%)

13.32 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

98.7
/packages/server/src/ruleExecutor.ts
1
import { Lock } from 'async-await-mutex-lock';
1✔
2
import { AppError, DEFAULT_RULE_PRIORITY, HttpCode, errorToLog, logger } from '@stuntman/shared';
1✔
3
import type * as Stuntman from '@stuntman/shared';
1✔
4
import { CUSTOM_RULES, DEFAULT_RULES } from './rules';
1✔
5

1✔
6
const ruleExecutors: Record<string, RuleExecutor> = {};
1✔
7

1✔
8
const transformMockRuleToLive = (rule: Stuntman.Rule): Stuntman.LiveRule => {
1✔
9
    return {
184✔
10
        ...rule,
184✔
11
        counter: 0,
184✔
12
        isEnabled: rule.isEnabled ?? true,
184✔
13
        createdTimestamp: Date.now(),
184✔
14
    };
184✔
15
};
184✔
16

1✔
17
class RuleExecutor implements Stuntman.RuleExecutorInterface {
1✔
18
    // TODO persistent rule storage maybe
20✔
19
    private _rules: Stuntman.LiveRule[];
20✔
20
    private rulesLock = new Lock();
20✔
21

20✔
22
    private get enabledRules(): readonly Stuntman.LiveRule[] {
20✔
23
        if (!this._rules) {
33✔
24
            this._rules = new Array<Stuntman.LiveRule>();
1✔
25
        }
1✔
26
        const now = Date.now();
33✔
27
        return this._rules
33✔
28
            .filter((r) => r.isEnabled && (!Number.isFinite(r.ttlSeconds) || r.createdTimestamp + r.ttlSeconds * 1000 > now))
33✔
29
            .sort((a, b) => (a.priority ?? DEFAULT_RULE_PRIORITY) - (b.priority ?? DEFAULT_RULE_PRIORITY));
33✔
30
    }
33✔
31

20✔
32
    constructor(rules?: Stuntman.Rule[]) {
20✔
33
        this._rules = (rules || []).map(transformMockRuleToLive);
20✔
34
    }
20✔
35

20✔
36
    private hasExpired() {
20✔
37
        const now = Date.now();
164✔
38
        return this._rules.some((r) => Number.isFinite(r.ttlSeconds) && r.createdTimestamp + r.ttlSeconds * 1000 < now);
164✔
39
    }
164✔
40

20✔
41
    private async cleanUpExpired() {
20✔
42
        if (!this.hasExpired()) {
164✔
43
            return;
163✔
44
        }
163✔
45
        await this.rulesLock.acquire();
1✔
46
        const now = Date.now();
1✔
47
        try {
1✔
48
            this._rules = this._rules.filter((r) => {
1✔
49
                const shouldKeep = !Number.isFinite(r.ttlSeconds) || r.createdTimestamp + r.ttlSeconds * 1000 > now;
102✔
50
                if (!shouldKeep) {
102✔
51
                    logger.debug({ ruleId: r.id }, 'removing expired rule');
52✔
52
                }
52✔
53
                return shouldKeep;
102✔
54
            });
1✔
55
        } finally {
1✔
56
            await this.rulesLock.release();
1✔
57
        }
1✔
58
    }
164✔
59

20✔
60
    async addRule(rule: Stuntman.Rule, overwrite?: boolean): Promise<Stuntman.LiveRule> {
20✔
61
        await this.cleanUpExpired();
149✔
62
        await this.rulesLock.acquire();
149✔
63
        try {
149✔
64
            if (this._rules.some((r) => r.id === rule.id)) {
149✔
65
                if (!overwrite) {
2✔
66
                    throw new AppError({ httpCode: HttpCode.CONFLICT, message: 'rule with given ID already exists' });
1✔
67
                }
1✔
68
                this._removeRule(rule.id);
1✔
69
            }
1✔
70
            const liveRule = transformMockRuleToLive(rule);
148✔
71
            this._rules.push(liveRule);
148✔
72
            logger.debug(liveRule, 'rule added');
148✔
73
            return liveRule;
148✔
74
        } finally {
148✔
75
            await this.rulesLock.release();
149✔
76
        }
149✔
77
    }
149✔
78

20✔
79
    private _removeRule(ruleOrId: string | Stuntman.Rule) {
20✔
80
        this._rules = this._rules.filter((r) => {
8✔
81
            const notFound = r.id !== (typeof ruleOrId === 'string' ? ruleOrId : ruleOrId.id);
44✔
82
            if (!notFound) {
44✔
83
                logger.debug({ ruleId: r.id }, 'rule removed');
8✔
84
            }
8✔
85
            return notFound;
44✔
86
        });
8✔
87
    }
8✔
88

20✔
89
    async removeRule(id: string): Promise<void>;
20✔
90
    async removeRule(rule: Stuntman.Rule): Promise<void>;
20✔
91
    async removeRule(ruleOrId: string | Stuntman.Rule): Promise<void> {
20✔
92
        await this.cleanUpExpired();
7✔
93
        await this.rulesLock.acquire();
7✔
94
        try {
7✔
95
            this._removeRule(ruleOrId);
7✔
96
        } finally {
7✔
97
            await this.rulesLock.release();
7✔
98
        }
7✔
99
    }
7✔
100

20✔
101
    enableRule(id: string): void;
20✔
102
    enableRule(rule: Stuntman.Rule): void;
20✔
103
    enableRule(ruleOrId: string | Stuntman.Rule): void {
20✔
104
        const ruleId = typeof ruleOrId === 'string' ? ruleOrId : ruleOrId.id;
3✔
105
        this._rules.forEach((r) => {
3✔
106
            if (r.id === ruleId) {
28✔
107
                r.counter = 0;
3✔
108
                r.isEnabled = true;
3✔
109
                logger.debug({ ruleId: r.id }, 'rule enabled');
3✔
110
            }
3✔
111
        });
3✔
112
    }
3✔
113

20✔
114
    disableRule(id: string): void;
20✔
115
    disableRule(rule: Stuntman.Rule): void;
20✔
116
    disableRule(ruleOrId: string | Stuntman.Rule): void {
20✔
117
        const ruleId = typeof ruleOrId === 'string' ? ruleOrId : ruleOrId.id;
13✔
118
        this._rules.forEach((r) => {
13✔
119
            if (r.id === ruleId) {
148✔
120
                r.isEnabled = false;
13✔
121
                logger.debug({ ruleId: r.id }, 'rule disabled');
13✔
122
            }
13✔
123
        });
13✔
124
    }
13✔
125

20✔
126
    async findMatchingRule(request: Stuntman.Request): Promise<Stuntman.LiveRule | null> {
20✔
127
        const logContext: Record<string, any> = {
27✔
128
            requestId: request.id,
27✔
129
        };
27✔
130
        let dynamicLabels: string[] = [];
27✔
131
        const matchingRule = this.enabledRules.find((rule) => {
27✔
132
            try {
40✔
133
                const matchResult = rule.matches(request);
40✔
134
                logger.trace({ ...logContext, matchResult }, `rule match attempt for ${rule.id}`);
40✔
135
                if (typeof matchResult === 'boolean') {
40✔
136
                    return matchResult;
36✔
137
                }
36✔
138
                if (matchResult.labels) {
38!
139
                    dynamicLabels = matchResult.labels;
×
140
                }
×
141
                return matchResult.result;
3✔
142
            } catch (error) {
38✔
143
                logger.error(
1✔
144
                    { ...logContext, ruleId: rule.id, error: errorToLog(error as Error) },
1✔
145
                    'error in rule match function'
1✔
146
                );
1✔
147
            }
1✔
148
            return undefined;
1✔
149
        });
27✔
150
        if (!matchingRule) {
27✔
151
            logger.debug(logContext, 'no matching rule found');
1✔
152
            return null;
1✔
153
        }
1✔
154
        const matchResult: Stuntman.RuleMatchResult = matchingRule.matches(request);
26✔
155
        logContext.ruleId = matchingRule.id;
26✔
156
        logger.debug(
26✔
157
            { ...logContext, matchResultMessage: typeof matchResult !== 'boolean' ? matchResult.description : null },
27✔
158
            'found matching rule'
27✔
159
        );
27✔
160
        const matchingRuleClone = Object.freeze(
27✔
161
            Object.assign({}, matchingRule, {
27✔
162
                labels: matchingRule.labels ? [...(matchingRule.labels || []), ...dynamicLabels] : dynamicLabels,
27!
163
            })
27✔
164
        );
27✔
165
        ++matchingRule.counter;
27✔
166
        logContext.ruleCounter = matchingRule.counter;
27✔
167
        if (Number.isNaN(matchingRule.counter) || !Number.isFinite(matchingRule.counter)) {
27✔
168
            matchingRule.counter = 0;
1✔
169
            logger.warn(logContext, "it's over 9000!!!");
1✔
170
        }
1✔
171
        if (matchingRule.disableAfterUse) {
27✔
172
            if (matchingRule.disableAfterUse === true || matchingRule.disableAfterUse <= matchingRule.counter) {
4✔
173
                logger.debug(logContext, 'disabling rule for future requests');
2✔
174
                matchingRule.isEnabled = false;
2✔
175
            }
2✔
176
        }
4✔
177
        if (matchingRule.removeAfterUse) {
27✔
178
            if (matchingRule.removeAfterUse === true || matchingRule.removeAfterUse <= matchingRule.counter) {
8✔
179
                logger.debug(logContext, 'removing rule for future requests');
6✔
180
                await this.removeRule(matchingRule);
6✔
181
            }
6✔
182
        }
8✔
183
        if (typeof matchResult !== 'boolean') {
27✔
184
            if (matchResult.disableRuleIds && matchResult.disableRuleIds.length > 0) {
3✔
185
                logger.debug(
1✔
186
                    { ...logContext, disableRuleIds: matchResult.disableRuleIds },
1✔
187
                    'disabling rules based on matchResult'
1✔
188
                );
1✔
189
                for (const ruleId of matchResult.disableRuleIds) {
1✔
190
                    this.disableRule(ruleId);
1✔
191
                }
1✔
192
            }
1✔
193
            if (matchResult.enableRuleIds && matchResult.enableRuleIds.length > 0) {
3✔
194
                logger.debug({ ...logContext, disableRuleIds: matchResult.enableRuleIds }, 'enabling rules based on matchResult');
1✔
195
                for (const ruleId of matchResult.enableRuleIds) {
1✔
196
                    this.enableRule(ruleId);
1✔
197
                }
1✔
198
            }
1✔
199
        }
3✔
200
        return matchingRuleClone;
26✔
201
    }
26✔
202

20✔
203
    async getRules(): Promise<readonly Stuntman.LiveRule[]> {
20✔
204
        await this.cleanUpExpired();
6✔
205
        return this._rules;
6✔
206
    }
6✔
207

20✔
208
    async getRule(id: string): Promise<Stuntman.LiveRule | undefined> {
20✔
209
        await this.cleanUpExpired();
1✔
210
        return this._rules.find((r) => r.id === id);
1✔
211
    }
1✔
212
}
20✔
213

1✔
214
export const getRuleExecutor = (mockUuid: string, overrideRules?: Stuntman.DeployedRule[]): RuleExecutor => {
1✔
215
    if (!ruleExecutors[mockUuid]) {
20✔
216
        if (overrideRules === null) {
20✔
217
            ruleExecutors[mockUuid] = new RuleExecutor();
1✔
218
        } else {
20✔
219
            ruleExecutors[mockUuid] = new RuleExecutor(
19✔
220
                (overrideRules ?? [...DEFAULT_RULES, ...CUSTOM_RULES]).map((r) => ({ ...r, ttlSeconds: Infinity }))
19✔
221
            );
19✔
222
        }
19✔
223
    }
20✔
224
    return ruleExecutors[mockUuid]!;
20✔
225
};
20✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc