@bytebrand/export-cars 1.26.28 → 1.26.30
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 +100 -0
- package/dist/configs/index.d.ts +1 -0
- package/dist/configs/index.js +1 -0
- package/dist/configs/index.js.map +1 -1
- package/dist/configs/openai-ads.config.d.ts +22 -0
- package/dist/configs/openai-ads.config.js +56 -0
- package/dist/configs/openai-ads.config.js.map +1 -0
- package/dist/enums/export-type.enum.d.ts +2 -1
- package/dist/enums/export-type.enum.js +1 -0
- package/dist/enums/export-type.enum.js.map +1 -1
- package/dist/enums/image-type.enum.d.ts +2 -1
- package/dist/enums/image-type.enum.js +1 -0
- package/dist/enums/image-type.enum.js.map +1 -1
- package/dist/export.schema.d.ts +6 -2
- package/dist/export.schema.js +4 -0
- package/dist/export.schema.js.map +1 -1
- package/dist/exporters/index.d.ts +1 -0
- package/dist/exporters/index.js +1 -0
- package/dist/exporters/index.js.map +1 -1
- package/dist/exporters/openai-ads.exporter.d.ts +13 -0
- package/dist/exporters/openai-ads.exporter.js +91 -0
- package/dist/exporters/openai-ads.exporter.js.map +1 -0
- package/dist/formats/openai-ads-csv.builder.d.ts +15 -0
- package/dist/formats/openai-ads-csv.builder.js +48 -0
- package/dist/formats/openai-ads-csv.builder.js.map +1 -0
- package/dist/interfaces/openai-ads.interface.d.ts +35 -0
- package/dist/interfaces/openai-ads.interface.js +37 -0
- package/dist/interfaces/openai-ads.interface.js.map +1 -0
- package/dist/interfaces/params.interface.d.ts +10 -0
- package/dist/services/feed-guard.d.ts +44 -0
- package/dist/services/feed-guard.js +156 -0
- package/dist/services/feed-guard.js.map +1 -0
- package/dist/services/image-url-validator.service.d.ts +45 -0
- package/dist/services/image-url-validator.service.js +141 -0
- package/dist/services/image-url-validator.service.js.map +1 -0
- package/dist/services/images.service.d.ts +12 -0
- package/dist/services/images.service.js +46 -1
- package/dist/services/images.service.js.map +1 -1
- package/dist/services/index.d.ts +1 -0
- package/dist/services/index.js +1 -0
- package/dist/services/index.js.map +1 -1
- package/dist/services/openai-ads-feed-run.d.ts +74 -0
- package/dist/services/openai-ads-feed-run.js +151 -0
- package/dist/services/openai-ads-feed-run.js.map +1 -0
- package/dist/services/openai-ads-ledger.d.ts +19 -0
- package/dist/services/openai-ads-ledger.js +72 -0
- package/dist/services/openai-ads-ledger.js.map +1 -0
- package/dist/services/publication-gate.js +8 -8
- package/dist/transformers/openai-ads.transformer.d.ts +29 -0
- package/dist/transformers/openai-ads.transformer.js +166 -0
- package/dist/transformers/openai-ads.transformer.js.map +1 -0
- package/dist/transformers/openai-image-fields.d.ts +16 -0
- package/dist/transformers/openai-image-fields.js +34 -0
- package/dist/transformers/openai-image-fields.js.map +1 -0
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/docs/openai-ads/AUDIT.md +235 -0
- package/docs/openai-ads/RUNBOOK.md +94 -0
- package/package.json +64 -64
- package/schema/feed-items.json +134 -134
- package/src/configs/config.ts +99 -99
- package/src/configs/index.ts +1 -0
- package/src/configs/openai-ads.config.ts +70 -0
- package/src/enums/export-type.enum.ts +2 -0
- package/src/enums/image-type.enum.ts +5 -0
- package/src/export.schema.ts +6 -0
- package/src/exporters/index.ts +1 -0
- package/src/exporters/openai-ads.exporter.ts +93 -0
- package/src/formats/openai-ads-csv.builder.ts +55 -0
- package/src/interfaces/openai-ads.interface.ts +64 -0
- package/src/interfaces/params.interface.ts +13 -0
- package/src/services/feed-guard.ts +219 -0
- package/src/services/image-url-validator.service.ts +181 -0
- package/src/services/images.service.ts +72 -0
- package/src/services/index.ts +1 -0
- package/src/services/openai-ads-feed-run.ts +226 -0
- package/src/services/openai-ads-ledger.ts +116 -0
- package/src/services/publication-gate.ts +119 -119
- package/src/transformers/openai-ads.transformer.ts +243 -0
- package/src/transformers/openai-image-fields.ts +60 -0
- package/test/clean-images.test.ts +86 -0
- package/test/fixtures/images-service.harness.ts +32 -0
- package/test/fixtures/images.golden.json +218 -0
- package/test/fixtures/imbo-images.fixture.ts +77 -0
- package/test/fixtures/openai-ads.fixture.ts +89 -0
- package/test/image-url-validator.test.ts +110 -0
- package/test/images-regression.test.ts +23 -0
- package/test/openai-ads-feed-run.test.ts +278 -0
- package/test/openai-ads-mapping.test.ts +157 -0
- package/test/openai-image-fields.test.ts +99 -0
- package/test/publication-gate.test.ts +86 -86
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
{
|
|
2
|
+
"full": {
|
|
3
|
+
"defaultWithPlaceholders": [
|
|
4
|
+
"https://images.auto.de/carimage/2ce1d71f-851f-4123-ae16-187ddd7923c5/extRight0001/large",
|
|
5
|
+
"https://images.auto.de/carimage/2ce1d71f-851f-4123-ae16-187ddd7923c5/extCowl00001/large",
|
|
6
|
+
"https://images.auto.de/carimage/2ce1d71f-851f-4123-ae16-187ddd7923c5/extLeft00001/large",
|
|
7
|
+
"https://images.auto.de/carimage/2ce1d71f-851f-4123-ae16-187ddd7923c5/extSide00001/large",
|
|
8
|
+
"https://images.auto.de/carimage/2ce1d71f-851f-4123-ae16-187ddd7923c5/extBack00001/large",
|
|
9
|
+
"https://images.auto.de/carimage/2ce1d71f-851f-4123-ae16-187ddd7923c5/extOpened001/large",
|
|
10
|
+
"https://images.auto.de/carimage/2ce1d71f-851f-4123-ae16-187ddd7923c5/conext000001/large",
|
|
11
|
+
"https://images.auto.de/carimage/2ce1d71f-851f-4123-ae16-187ddd7923c5/engine000001/large",
|
|
12
|
+
"https://images.auto.de/carimage/2ce1d71f-851f-4123-ae16-187ddd7923c5/intDash00001/large",
|
|
13
|
+
"https://images.auto.de/carimage/2ce1d71f-851f-4123-ae16-187ddd7923c5/intSeats0001/large",
|
|
14
|
+
"https://images.auto.de/carimage/2ce1d71f-851f-4123-ae16-187ddd7923c5/intTrunk0001/large",
|
|
15
|
+
"https://images.auto.de/carimage/2ce1d71f-851f-4123-ae16-187ddd7923c5/seatHeat0001/large",
|
|
16
|
+
"https://images.auto.de/carimage/2ce1d71f-851f-4123-ae16-187ddd7923c5/featRims0001/large",
|
|
17
|
+
"https://images.auto.de/carimage/2ce1d71f-851f-4123-ae16-187ddd7923c5/simple000001/large",
|
|
18
|
+
"https://images.auto.de/carimage/noimage/7pNvWELuWhZX/large-cached.jpeg",
|
|
19
|
+
"https://images.auto.de/carimage/noimage/GvyXmrpaf1D6/large-cached.jpeg",
|
|
20
|
+
"https://images.auto.de/carimage/noimage/pFyfBsqBHEso/large-cached.jpeg"
|
|
21
|
+
],
|
|
22
|
+
"defaultIgnorePlaceholders": [
|
|
23
|
+
"https://images.auto.de/carimage/2ce1d71f-851f-4123-ae16-187ddd7923c5/extRight0001/large",
|
|
24
|
+
"https://images.auto.de/carimage/2ce1d71f-851f-4123-ae16-187ddd7923c5/extCowl00001/large",
|
|
25
|
+
"https://images.auto.de/carimage/2ce1d71f-851f-4123-ae16-187ddd7923c5/extLeft00001/large",
|
|
26
|
+
"https://images.auto.de/carimage/2ce1d71f-851f-4123-ae16-187ddd7923c5/extSide00001/large",
|
|
27
|
+
"https://images.auto.de/carimage/2ce1d71f-851f-4123-ae16-187ddd7923c5/extBack00001/large",
|
|
28
|
+
"https://images.auto.de/carimage/2ce1d71f-851f-4123-ae16-187ddd7923c5/extOpened001/large",
|
|
29
|
+
"https://images.auto.de/carimage/2ce1d71f-851f-4123-ae16-187ddd7923c5/conext000001/large",
|
|
30
|
+
"https://images.auto.de/carimage/2ce1d71f-851f-4123-ae16-187ddd7923c5/engine000001/large",
|
|
31
|
+
"https://images.auto.de/carimage/2ce1d71f-851f-4123-ae16-187ddd7923c5/intDash00001/large",
|
|
32
|
+
"https://images.auto.de/carimage/2ce1d71f-851f-4123-ae16-187ddd7923c5/intSeats0001/large",
|
|
33
|
+
"https://images.auto.de/carimage/2ce1d71f-851f-4123-ae16-187ddd7923c5/intTrunk0001/large",
|
|
34
|
+
"https://images.auto.de/carimage/2ce1d71f-851f-4123-ae16-187ddd7923c5/seatHeat0001/large",
|
|
35
|
+
"https://images.auto.de/carimage/2ce1d71f-851f-4123-ae16-187ddd7923c5/featRims0001/large",
|
|
36
|
+
"https://images.auto.de/carimage/2ce1d71f-851f-4123-ae16-187ddd7923c5/simple000001/large"
|
|
37
|
+
],
|
|
38
|
+
"marketing": [
|
|
39
|
+
"https://images.auto.de/generated_images/2/c/e/2ce1d71f-851f-4123-ae16-187ddd7923c5/criteo-channel-extRight0001.jpg",
|
|
40
|
+
"https://images.auto.de/generated_images/2/c/e/2ce1d71f-851f-4123-ae16-187ddd7923c5/criteo-channel-extBack00001.jpg",
|
|
41
|
+
"https://images.auto.de/generated_images/2/c/e/2ce1d71f-851f-4123-ae16-187ddd7923c5/criteo-channel-intDash00001.jpg",
|
|
42
|
+
"https://images.auto.de/carimage/noimage/7pNvWELuWhZX/large-cached.jpeg",
|
|
43
|
+
"https://images.auto.de/carimage/noimage/GvyXmrpaf1D6/large-cached.jpeg",
|
|
44
|
+
"https://images.auto.de/carimage/noimage/pFyfBsqBHEso/large-cached.jpeg"
|
|
45
|
+
],
|
|
46
|
+
"googleExtInt": {
|
|
47
|
+
"extMain": [
|
|
48
|
+
"https://images.auto.de/carimage/2ce1d71f-851f-4123-ae16-187ddd7923c5/extRight0001/small",
|
|
49
|
+
"https://images.auto.de/carimage/2ce1d71f-851f-4123-ae16-187ddd7923c5/extLeft00001/small",
|
|
50
|
+
"https://images.auto.de/carimage/2ce1d71f-851f-4123-ae16-187ddd7923c5/extOpened001/small"
|
|
51
|
+
],
|
|
52
|
+
"ext": [
|
|
53
|
+
"https://images.auto.de/carimage/2ce1d71f-851f-4123-ae16-187ddd7923c5/extCowl00001/small",
|
|
54
|
+
"https://images.auto.de/carimage/2ce1d71f-851f-4123-ae16-187ddd7923c5/extSide00001/small",
|
|
55
|
+
"https://images.auto.de/carimage/2ce1d71f-851f-4123-ae16-187ddd7923c5/extBack00001/small"
|
|
56
|
+
],
|
|
57
|
+
"int": [
|
|
58
|
+
"https://images.auto.de/carimage/2ce1d71f-851f-4123-ae16-187ddd7923c5/intDash00001/small",
|
|
59
|
+
"https://images.auto.de/carimage/2ce1d71f-851f-4123-ae16-187ddd7923c5/intSeats0001/small",
|
|
60
|
+
"https://images.auto.de/carimage/2ce1d71f-851f-4123-ae16-187ddd7923c5/intTrunk0001/small"
|
|
61
|
+
],
|
|
62
|
+
"feat": [
|
|
63
|
+
"https://images.auto.de/carimage/2ce1d71f-851f-4123-ae16-187ddd7923c5/conext000001/small",
|
|
64
|
+
"https://images.auto.de/carimage/2ce1d71f-851f-4123-ae16-187ddd7923c5/engine000001/small",
|
|
65
|
+
"https://images.auto.de/carimage/2ce1d71f-851f-4123-ae16-187ddd7923c5/seatHeat0001/small",
|
|
66
|
+
"https://images.auto.de/carimage/2ce1d71f-851f-4123-ae16-187ddd7923c5/featRims0001/small"
|
|
67
|
+
]
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
"leftOnly": {
|
|
71
|
+
"defaultWithPlaceholders": [
|
|
72
|
+
"https://images.auto.de/carimage/3703a23a-654d-426e-801e-beeffcc1e9c8/extLeft00002/large",
|
|
73
|
+
"https://images.auto.de/carimage/3703a23a-654d-426e-801e-beeffcc1e9c8/extBack00002/large",
|
|
74
|
+
"https://images.auto.de/carimage/3703a23a-654d-426e-801e-beeffcc1e9c8/intDash00002/large",
|
|
75
|
+
"https://images.auto.de/carimage/noimage/7pNvWELuWhZX/large-cached.jpeg",
|
|
76
|
+
"https://images.auto.de/carimage/noimage/GvyXmrpaf1D6/large-cached.jpeg",
|
|
77
|
+
"https://images.auto.de/carimage/noimage/pFyfBsqBHEso/large-cached.jpeg"
|
|
78
|
+
],
|
|
79
|
+
"defaultIgnorePlaceholders": [
|
|
80
|
+
"https://images.auto.de/carimage/3703a23a-654d-426e-801e-beeffcc1e9c8/extLeft00002/large",
|
|
81
|
+
"https://images.auto.de/carimage/3703a23a-654d-426e-801e-beeffcc1e9c8/extBack00002/large",
|
|
82
|
+
"https://images.auto.de/carimage/3703a23a-654d-426e-801e-beeffcc1e9c8/intDash00002/large"
|
|
83
|
+
],
|
|
84
|
+
"marketing": [
|
|
85
|
+
"https://images.auto.de/generated_images/3/7/0/3703a23a-654d-426e-801e-beeffcc1e9c8/criteo-channel-extLeft00002.jpg",
|
|
86
|
+
"https://images.auto.de/generated_images/3/7/0/3703a23a-654d-426e-801e-beeffcc1e9c8/criteo-channel-extBack00002.jpg",
|
|
87
|
+
"https://images.auto.de/generated_images/3/7/0/3703a23a-654d-426e-801e-beeffcc1e9c8/criteo-channel-intDash00002.jpg",
|
|
88
|
+
"https://images.auto.de/carimage/noimage/7pNvWELuWhZX/large-cached.jpeg",
|
|
89
|
+
"https://images.auto.de/carimage/noimage/GvyXmrpaf1D6/large-cached.jpeg",
|
|
90
|
+
"https://images.auto.de/carimage/noimage/pFyfBsqBHEso/large-cached.jpeg"
|
|
91
|
+
],
|
|
92
|
+
"googleExtInt": {
|
|
93
|
+
"extMain": [
|
|
94
|
+
"https://images.auto.de/carimage/3703a23a-654d-426e-801e-beeffcc1e9c8/extLeft00002/small"
|
|
95
|
+
],
|
|
96
|
+
"ext": [
|
|
97
|
+
"https://images.auto.de/carimage/3703a23a-654d-426e-801e-beeffcc1e9c8/extBack00002/small"
|
|
98
|
+
],
|
|
99
|
+
"int": [
|
|
100
|
+
"https://images.auto.de/carimage/3703a23a-654d-426e-801e-beeffcc1e9c8/intDash00002/small"
|
|
101
|
+
],
|
|
102
|
+
"feat": []
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
"noHero": {
|
|
106
|
+
"defaultWithPlaceholders": [
|
|
107
|
+
"https://images.auto.de/carimage/2c91c177-ddd3-4224-8b04-bdb96bf1b64c/extBack00003/large",
|
|
108
|
+
"https://images.auto.de/carimage/2c91c177-ddd3-4224-8b04-bdb96bf1b64c/intDash00003/large",
|
|
109
|
+
"https://images.auto.de/carimage/2c91c177-ddd3-4224-8b04-bdb96bf1b64c/featRims0003/large",
|
|
110
|
+
"https://images.auto.de/carimage/noimage/7pNvWELuWhZX/large-cached.jpeg",
|
|
111
|
+
"https://images.auto.de/carimage/noimage/GvyXmrpaf1D6/large-cached.jpeg",
|
|
112
|
+
"https://images.auto.de/carimage/noimage/pFyfBsqBHEso/large-cached.jpeg"
|
|
113
|
+
],
|
|
114
|
+
"defaultIgnorePlaceholders": [
|
|
115
|
+
"https://images.auto.de/carimage/2c91c177-ddd3-4224-8b04-bdb96bf1b64c/extBack00003/large",
|
|
116
|
+
"https://images.auto.de/carimage/2c91c177-ddd3-4224-8b04-bdb96bf1b64c/intDash00003/large",
|
|
117
|
+
"https://images.auto.de/carimage/2c91c177-ddd3-4224-8b04-bdb96bf1b64c/featRims0003/large"
|
|
118
|
+
],
|
|
119
|
+
"marketing": [
|
|
120
|
+
"https://images.auto.de/generated_images/2/c/9/2c91c177-ddd3-4224-8b04-bdb96bf1b64c/criteo-channel-extBack00003.jpg",
|
|
121
|
+
"https://images.auto.de/generated_images/2/c/9/2c91c177-ddd3-4224-8b04-bdb96bf1b64c/criteo-channel-intDash00003.jpg",
|
|
122
|
+
"https://images.auto.de/carimage/noimage/7pNvWELuWhZX/large-cached.jpeg",
|
|
123
|
+
"https://images.auto.de/carimage/noimage/GvyXmrpaf1D6/large-cached.jpeg",
|
|
124
|
+
"https://images.auto.de/carimage/noimage/pFyfBsqBHEso/large-cached.jpeg"
|
|
125
|
+
],
|
|
126
|
+
"googleExtInt": {
|
|
127
|
+
"extMain": [],
|
|
128
|
+
"ext": [
|
|
129
|
+
"https://images.auto.de/carimage/2c91c177-ddd3-4224-8b04-bdb96bf1b64c/extBack00003/small"
|
|
130
|
+
],
|
|
131
|
+
"int": [
|
|
132
|
+
"https://images.auto.de/carimage/2c91c177-ddd3-4224-8b04-bdb96bf1b64c/intDash00003/small"
|
|
133
|
+
],
|
|
134
|
+
"feat": [
|
|
135
|
+
"https://images.auto.de/carimage/2c91c177-ddd3-4224-8b04-bdb96bf1b64c/featRims0003/small"
|
|
136
|
+
]
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
"duplicates": {
|
|
140
|
+
"defaultWithPlaceholders": [
|
|
141
|
+
"https://images.auto.de/carimage/2e101cac-fb72-4cd7-a598-2ebba983a4f9/extRight0004/large",
|
|
142
|
+
"https://images.auto.de/carimage/2e101cac-fb72-4cd7-a598-2ebba983a4f9/extRight0004/large",
|
|
143
|
+
"https://images.auto.de/carimage/2e101cac-fb72-4cd7-a598-2ebba983a4f9/extRight0005/large",
|
|
144
|
+
"https://images.auto.de/carimage/2e101cac-fb72-4cd7-a598-2ebba983a4f9/intDash00004/large",
|
|
145
|
+
"https://images.auto.de/carimage/noimage/7pNvWELuWhZX/large-cached.jpeg",
|
|
146
|
+
"https://images.auto.de/carimage/noimage/GvyXmrpaf1D6/large-cached.jpeg",
|
|
147
|
+
"https://images.auto.de/carimage/noimage/pFyfBsqBHEso/large-cached.jpeg"
|
|
148
|
+
],
|
|
149
|
+
"defaultIgnorePlaceholders": [
|
|
150
|
+
"https://images.auto.de/carimage/2e101cac-fb72-4cd7-a598-2ebba983a4f9/extRight0004/large",
|
|
151
|
+
"https://images.auto.de/carimage/2e101cac-fb72-4cd7-a598-2ebba983a4f9/extRight0004/large",
|
|
152
|
+
"https://images.auto.de/carimage/2e101cac-fb72-4cd7-a598-2ebba983a4f9/extRight0005/large",
|
|
153
|
+
"https://images.auto.de/carimage/2e101cac-fb72-4cd7-a598-2ebba983a4f9/intDash00004/large"
|
|
154
|
+
],
|
|
155
|
+
"marketing": [
|
|
156
|
+
"https://images.auto.de/generated_images/2/e/1/2e101cac-fb72-4cd7-a598-2ebba983a4f9/criteo-channel-extRight0005.jpg",
|
|
157
|
+
"https://images.auto.de/generated_images/2/e/1/2e101cac-fb72-4cd7-a598-2ebba983a4f9/criteo-channel-intDash00004.jpg",
|
|
158
|
+
"https://images.auto.de/carimage/noimage/7pNvWELuWhZX/large-cached.jpeg",
|
|
159
|
+
"https://images.auto.de/carimage/noimage/GvyXmrpaf1D6/large-cached.jpeg",
|
|
160
|
+
"https://images.auto.de/carimage/noimage/pFyfBsqBHEso/large-cached.jpeg"
|
|
161
|
+
],
|
|
162
|
+
"googleExtInt": {
|
|
163
|
+
"extMain": [
|
|
164
|
+
"https://images.auto.de/carimage/2e101cac-fb72-4cd7-a598-2ebba983a4f9/extRight0004/small",
|
|
165
|
+
"https://images.auto.de/carimage/2e101cac-fb72-4cd7-a598-2ebba983a4f9/extRight0004/small",
|
|
166
|
+
"https://images.auto.de/carimage/2e101cac-fb72-4cd7-a598-2ebba983a4f9/extRight0005/small"
|
|
167
|
+
],
|
|
168
|
+
"ext": [],
|
|
169
|
+
"int": [
|
|
170
|
+
"https://images.auto.de/carimage/2e101cac-fb72-4cd7-a598-2ebba983a4f9/intDash00004/small"
|
|
171
|
+
],
|
|
172
|
+
"feat": []
|
|
173
|
+
}
|
|
174
|
+
},
|
|
175
|
+
"onlyUnknown": {
|
|
176
|
+
"defaultWithPlaceholders": [
|
|
177
|
+
"https://images.auto.de/carimage/61889cf8-2193-4c1e-8cdd-521ca2d38c9a/simple000006/large",
|
|
178
|
+
"https://images.auto.de/carimage/61889cf8-2193-4c1e-8cdd-521ca2d38c9a/promo0000006/large",
|
|
179
|
+
"https://images.auto.de/carimage/noimage/7pNvWELuWhZX/large-cached.jpeg",
|
|
180
|
+
"https://images.auto.de/carimage/noimage/GvyXmrpaf1D6/large-cached.jpeg",
|
|
181
|
+
"https://images.auto.de/carimage/noimage/pFyfBsqBHEso/large-cached.jpeg"
|
|
182
|
+
],
|
|
183
|
+
"defaultIgnorePlaceholders": [
|
|
184
|
+
"https://images.auto.de/carimage/61889cf8-2193-4c1e-8cdd-521ca2d38c9a/simple000006/large",
|
|
185
|
+
"https://images.auto.de/carimage/61889cf8-2193-4c1e-8cdd-521ca2d38c9a/promo0000006/large"
|
|
186
|
+
],
|
|
187
|
+
"marketing": [
|
|
188
|
+
"https://images.auto.de/carimage/noimage/7pNvWELuWhZX/large-cached.jpeg",
|
|
189
|
+
"https://images.auto.de/carimage/noimage/GvyXmrpaf1D6/large-cached.jpeg",
|
|
190
|
+
"https://images.auto.de/carimage/noimage/pFyfBsqBHEso/large-cached.jpeg"
|
|
191
|
+
],
|
|
192
|
+
"googleExtInt": {
|
|
193
|
+
"extMain": [],
|
|
194
|
+
"ext": [],
|
|
195
|
+
"int": [],
|
|
196
|
+
"feat": []
|
|
197
|
+
}
|
|
198
|
+
},
|
|
199
|
+
"empty": {
|
|
200
|
+
"defaultWithPlaceholders": [
|
|
201
|
+
"https://images.auto.de/carimage/noimage/7pNvWELuWhZX/large-cached.jpeg",
|
|
202
|
+
"https://images.auto.de/carimage/noimage/GvyXmrpaf1D6/large-cached.jpeg",
|
|
203
|
+
"https://images.auto.de/carimage/noimage/pFyfBsqBHEso/large-cached.jpeg"
|
|
204
|
+
],
|
|
205
|
+
"defaultIgnorePlaceholders": [
|
|
206
|
+
"https://images.auto.de/carimage/noimage/7pNvWELuWhZX/large-cached.jpeg",
|
|
207
|
+
"https://images.auto.de/carimage/noimage/GvyXmrpaf1D6/large-cached.jpeg",
|
|
208
|
+
"https://images.auto.de/carimage/noimage/pFyfBsqBHEso/large-cached.jpeg"
|
|
209
|
+
],
|
|
210
|
+
"marketing": null,
|
|
211
|
+
"googleExtInt": {
|
|
212
|
+
"extMain": [],
|
|
213
|
+
"ext": [],
|
|
214
|
+
"int": [],
|
|
215
|
+
"feat": []
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { ImboImageResponseInterface } from '@bytebrand/chunkable';
|
|
2
|
+
|
|
3
|
+
// Imbo image lists as CarBasicDataCarsLoader hands them to the transformers (newest first, as Imbo
|
|
4
|
+
// returns them). Codes mirror real image_code_predicted labels, incl. the "-suffix" form.
|
|
5
|
+
const img = (imageIdentifier: string, imageCodePredicted?: string): ImboImageResponseInterface => ({
|
|
6
|
+
imageIdentifier,
|
|
7
|
+
imageCodePredicted,
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
export const IMBO_FIXTURES: Record<string, { carId: string; images: ImboImageResponseInterface[] }> = {
|
|
11
|
+
full: {
|
|
12
|
+
carId: '2ce1d71f-851f-4123-ae16-187ddd7923c5',
|
|
13
|
+
images: [
|
|
14
|
+
img('intDash00001', 'int_f_ipanel1'),
|
|
15
|
+
img('featRims0001', 'ext_feat_rims'),
|
|
16
|
+
img('extBack00001', 'ext_b_main'),
|
|
17
|
+
img('simple000001', 'simple_01'),
|
|
18
|
+
img('extLeft00001', 'ext_f_left'),
|
|
19
|
+
img('conext000001', 'conext_f_main'),
|
|
20
|
+
img('intSeats0001', 'int_f_driverseats-1'),
|
|
21
|
+
img('extCowl00001', 'ext_f_cowl'),
|
|
22
|
+
img('extOpened001', 'ext_f_main_opened'),
|
|
23
|
+
img('engine000001', 'ext_feat_engine'),
|
|
24
|
+
img('seatHeat0001', 'int_feat_seatheating'),
|
|
25
|
+
img('extRight0001', 'ext_f_right'),
|
|
26
|
+
img('extSide00001', 'ext_l_main'),
|
|
27
|
+
img('intTrunk0001', 'int_b_trunk'),
|
|
28
|
+
],
|
|
29
|
+
},
|
|
30
|
+
leftOnly: {
|
|
31
|
+
carId: '3703a23a-654d-426e-801e-beeffcc1e9c8',
|
|
32
|
+
images: [
|
|
33
|
+
img('intDash00002', 'int_f_ipanel1'),
|
|
34
|
+
img('extBack00002', 'ext_b_main'),
|
|
35
|
+
img('extLeft00002', 'ext_f_left'),
|
|
36
|
+
],
|
|
37
|
+
},
|
|
38
|
+
noHero: {
|
|
39
|
+
carId: '2c91c177-ddd3-4224-8b04-bdb96bf1b64c',
|
|
40
|
+
images: [
|
|
41
|
+
img('extBack00003', 'ext_b_main'),
|
|
42
|
+
img('intDash00003', 'int_f_ipanel1'),
|
|
43
|
+
img('featRims0003', 'ext_feat_rims'),
|
|
44
|
+
],
|
|
45
|
+
},
|
|
46
|
+
duplicates: {
|
|
47
|
+
carId: '2e101cac-fb72-4cd7-a598-2ebba983a4f9',
|
|
48
|
+
images: [
|
|
49
|
+
img('extRight0004', 'ext_f_right'),
|
|
50
|
+
img('extRight0004', 'ext_f_right'),
|
|
51
|
+
img('extRight0005', 'ext_f_right'),
|
|
52
|
+
img('intDash00004', 'int_f_ipanel1'),
|
|
53
|
+
],
|
|
54
|
+
},
|
|
55
|
+
onlyUnknown: {
|
|
56
|
+
carId: '61889cf8-2193-4c1e-8cdd-521ca2d38c9a',
|
|
57
|
+
images: [img('simple000006', 'simple_01'), img('promo0000006', 'promo_slide')],
|
|
58
|
+
},
|
|
59
|
+
empty: {
|
|
60
|
+
carId: '0b0999c6-2f02-4bc3-8852-2307e8a0232f',
|
|
61
|
+
images: [],
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
// Same shape as a stored MarketingImageConfig (criteo-like) for getMarketingImages.
|
|
66
|
+
export const MARKETING_CONFIG_FIXTURE = {
|
|
67
|
+
channelCode: 'criteo channel',
|
|
68
|
+
imageExtension: 'jpg',
|
|
69
|
+
marketingImages: [
|
|
70
|
+
{ imageCodes: ['ext_f_right', 'ext_f_left'] },
|
|
71
|
+
{ imageCodes: ['int_f_ipanel1'] },
|
|
72
|
+
{ imageCodes: ['ext_b_main'] },
|
|
73
|
+
],
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export const cloneImages = (images: ImboImageResponseInterface[]): ImboImageResponseInterface[] =>
|
|
77
|
+
images.map(i => ({ ...i }));
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { ImboImageResponseInterface } from '@bytebrand/chunkable';
|
|
2
|
+
|
|
3
|
+
// Minimal RFC 4180 reader for the tests: whole file -> rows of cells (quoted newlines kept).
|
|
4
|
+
export const parseCsv = (text: string): string[][] => {
|
|
5
|
+
const rows: string[][] = [];
|
|
6
|
+
let row: string[] = [];
|
|
7
|
+
let cell = '';
|
|
8
|
+
let quoted = false;
|
|
9
|
+
for (let i = 0; i < text.length; i++) {
|
|
10
|
+
const c = text[i];
|
|
11
|
+
if (quoted) {
|
|
12
|
+
if (c === '"' && text[i + 1] === '"') { cell += '"'; i++; } else if (c === '"') { quoted = false; } else { cell += c; }
|
|
13
|
+
} else if (c === '"') {
|
|
14
|
+
quoted = true;
|
|
15
|
+
} else if (c === ',') {
|
|
16
|
+
row.push(cell); cell = '';
|
|
17
|
+
} else if (c === '\n') {
|
|
18
|
+
row.push(cell); rows.push(row); row = []; cell = '';
|
|
19
|
+
} else {
|
|
20
|
+
cell += c;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
if (cell || row.length) { row.push(cell); rows.push(row); }
|
|
24
|
+
return rows;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export const csvObjects = (text: string): Record<string, string>[] => {
|
|
28
|
+
const [header, ...rows] = parseCsv(text);
|
|
29
|
+
return rows.map(cells => Object.fromEntries(header.map((name, i) => [name, cells[i]])));
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// German labels the TranslatorExtended returns for these selectors (checked against
|
|
33
|
+
// @bytebrand/i18n-dictionaries); anything else comes back as the key itself, like the real one.
|
|
34
|
+
const LABELS: Record<string, string> = {
|
|
35
|
+
selector_fuel_petrol: 'Benzin',
|
|
36
|
+
selector_fuel_diesel: 'Diesel',
|
|
37
|
+
selector_gearbox_manualShift: 'Manuell',
|
|
38
|
+
selector_gearbox_automatic: 'Automatik',
|
|
39
|
+
selector_category_sedan: 'Limousine',
|
|
40
|
+
selector_category_combi: 'Kombi',
|
|
41
|
+
selector_exteriorColor_black: 'Schwarz',
|
|
42
|
+
selector_exteriorColor_grey: 'Grau',
|
|
43
|
+
selector_unknown: 'Unbekannt',
|
|
44
|
+
};
|
|
45
|
+
export const translate = (key: string): string => LABELS[key] ?? key;
|
|
46
|
+
|
|
47
|
+
export const NOW = new Date('2026-10-12T08:00:00Z');
|
|
48
|
+
const MAR_2021 = Date.UTC(2021, 2, 15) / 1000;
|
|
49
|
+
|
|
50
|
+
export const car = (id: string, overrides: Record<string, any> = {}): any => {
|
|
51
|
+
const base: any = {
|
|
52
|
+
_id: id,
|
|
53
|
+
mainData: {
|
|
54
|
+
make: 'Volkswagen',
|
|
55
|
+
model: 'Golf',
|
|
56
|
+
subModel: '1.5 TSI Life',
|
|
57
|
+
condition: 'selector_condition_used',
|
|
58
|
+
firstRegistration: MAR_2021,
|
|
59
|
+
mileage: 45000,
|
|
60
|
+
category: 'selector_category_sedan',
|
|
61
|
+
},
|
|
62
|
+
metaData: { ownerId: 'dealer-1', imagesCount: 2, vehicleId: 'ABC123' },
|
|
63
|
+
price: { currentSalesPrice: 17990 },
|
|
64
|
+
rate: { monthlyInstallment: 199, paybackPeriod: 48 },
|
|
65
|
+
consumption: { fuel: 'selector_fuel_petrol' },
|
|
66
|
+
driveSuspension: { gearbox: 'selector_gearbox_manualShift' },
|
|
67
|
+
engineData: { powerKW: 110, powerPS: 150 },
|
|
68
|
+
exterior: { exteriorColor: 'selector_exteriorColor_black' },
|
|
69
|
+
offer: { offerAvailability: 'selector_unknown' },
|
|
70
|
+
state: { isOnline: true },
|
|
71
|
+
};
|
|
72
|
+
for (const [path, value] of Object.entries(overrides)) {
|
|
73
|
+
const keys = path.split('.');
|
|
74
|
+
let target = base;
|
|
75
|
+
keys.slice(0, -1).forEach((key) => { target[key] = target[key] ?? {}; target = target[key]; });
|
|
76
|
+
if (value === undefined) {
|
|
77
|
+
delete target[keys[keys.length - 1]];
|
|
78
|
+
} else {
|
|
79
|
+
target[keys[keys.length - 1]] = value;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return base;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
export const images = (prefix: string): ImboImageResponseInterface[] => [
|
|
86
|
+
{ imageIdentifier: `${prefix}Back`, imageCodePredicted: 'ext_b_main' },
|
|
87
|
+
{ imageIdentifier: `${prefix}Right`, imageCodePredicted: 'ext_f_right' },
|
|
88
|
+
{ imageIdentifier: `${prefix}Dash`, imageCodePredicted: 'int_f_ipanel1' },
|
|
89
|
+
];
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// Run: npm test (ts-node, no test framework needed)
|
|
2
|
+
// Local stub image host — no real network. requireHttps is switched off only for the stub server.
|
|
3
|
+
import * as assert from 'node:assert/strict';
|
|
4
|
+
import { createServer, IncomingMessage, ServerResponse } from 'http';
|
|
5
|
+
import { AddressInfo } from 'net';
|
|
6
|
+
import { ImageUrlRejectReason as R, ImageUrlValidator } from '../src/services/image-url-validator.service';
|
|
7
|
+
|
|
8
|
+
const hits: Record<string, number> = {};
|
|
9
|
+
const JPEG = Buffer.from([0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10]);
|
|
10
|
+
|
|
11
|
+
const routes: Record<string, (req: IncomingMessage, res: ServerResponse) => void> = {
|
|
12
|
+
'/ok.jpg': (_, res) => { res.writeHead(200, { 'Content-Type': 'image/jpeg' }); res.end(JPEG); },
|
|
13
|
+
'/ok.png': (_, res) => { res.writeHead(200, { 'Content-Type': 'image/png' }); res.end(); },
|
|
14
|
+
'/photo.webp': (_, res) => { res.writeHead(200, { 'Content-Type': 'image/webp' }); res.end(); },
|
|
15
|
+
'/page.html': (_, res) => { res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' }); res.end('<html>'); },
|
|
16
|
+
// images.auto.de answers unknown images exactly like this: 404 with a placeholder JPEG body
|
|
17
|
+
'/missing.jpg': (_, res) => { res.writeHead(404, { 'Content-Type': 'image/jpeg' }); res.end(JPEG); },
|
|
18
|
+
'/forbidden.jpg': (_, res) => { res.writeHead(403, { 'Content-Type': 'text/html' }); res.end('Just a moment...'); },
|
|
19
|
+
'/error.jpg': (_, res) => { res.writeHead(503); res.end(); },
|
|
20
|
+
'/moved.jpg': (_, res) => { res.writeHead(302, { Location: '/ok.jpg' }); res.end(); },
|
|
21
|
+
'/loop.jpg': (_, res) => { res.writeHead(302, { Location: '/loop.jpg' }); res.end(); },
|
|
22
|
+
'/slow.jpg': (_, res) => { setTimeout(() => { res.writeHead(200, { 'Content-Type': 'image/jpeg' }); res.end(); }, 400); },
|
|
23
|
+
'/no-head.jpg': (req, res) => {
|
|
24
|
+
if (req.method === 'HEAD') { res.writeHead(405); res.end(); return; }
|
|
25
|
+
res.writeHead(200, { 'Content-Type': 'image/jpeg' }); res.end(JPEG);
|
|
26
|
+
},
|
|
27
|
+
'/carimage/noimage/7pNvWELuWhZX/large-cached.jpeg': (_, res) => { res.writeHead(200, { 'Content-Type': 'image/jpeg' }); res.end(JPEG); },
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const server = createServer((req, res) => {
|
|
31
|
+
const path = req.url.split('?')[0];
|
|
32
|
+
hits[path] = (hits[path] ?? 0) + 1;
|
|
33
|
+
(routes[path] ?? ((_, r) => { r.writeHead(404); r.end(); }))(req, res);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
(async () => {
|
|
37
|
+
await new Promise<void>(resolve => server.listen(0, '127.0.0.1', resolve));
|
|
38
|
+
const base = `http://127.0.0.1:${(server.address() as AddressInfo).port}`;
|
|
39
|
+
const validator = (options = {}) => new ImageUrlValidator({ requireHttps: false, timeoutMs: 150, ...options });
|
|
40
|
+
const expectReject = async (path: string, reason: R, status?: number) => {
|
|
41
|
+
const check = await validator().validate(`${base}${path}`);
|
|
42
|
+
assert.equal(check.ok, false);
|
|
43
|
+
assert.equal(check.reason, reason);
|
|
44
|
+
if (status) assert.equal(check.status, status);
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const cases: Array<[string, () => Promise<void>]> = [
|
|
48
|
+
['200 image/jpeg and image/png are valid', async () => {
|
|
49
|
+
assert.equal((await validator().validate(`${base}/ok.jpg`)).ok, true);
|
|
50
|
+
assert.equal((await validator().validate(`${base}/ok.png`)).ok, true);
|
|
51
|
+
}],
|
|
52
|
+
['404 with an image/jpeg placeholder body is rejected on status', () => expectReject('/missing.jpg', R.httpStatus, 404)],
|
|
53
|
+
['403 (WAF / challenge) is rejected', () => expectReject('/forbidden.jpg', R.httpStatus, 403)],
|
|
54
|
+
['5xx is rejected', () => expectReject('/error.jpg', R.httpStatus, 503)],
|
|
55
|
+
['wrong MIME (HTML) is rejected', () => expectReject('/page.html', R.wrongMime)],
|
|
56
|
+
['WebP is rejected unless the partner allows it', async () => {
|
|
57
|
+
await expectReject('/photo.webp', R.wrongMime);
|
|
58
|
+
assert.equal((await validator({ allowedMimeTypes: ['image/webp'] }).validate(`${base}/photo.webp`)).ok, true);
|
|
59
|
+
}],
|
|
60
|
+
['a redirect within the limit is followed and the final URL kept', async () => {
|
|
61
|
+
const check = await validator().validate(`${base}/moved.jpg`);
|
|
62
|
+
assert.equal(check.ok, true);
|
|
63
|
+
assert.equal(check.finalUrl, `${base}/ok.jpg`);
|
|
64
|
+
}],
|
|
65
|
+
['redirect loop is rejected', () => expectReject('/loop.jpg', R.tooManyRedirects)],
|
|
66
|
+
['timeout is rejected', () => expectReject('/slow.jpg', R.timeout)],
|
|
67
|
+
['HEAD not allowed (405) falls back to GET', async () => {
|
|
68
|
+
assert.equal((await validator().validate(`${base}/no-head.jpg`)).ok, true);
|
|
69
|
+
}],
|
|
70
|
+
['placeholder URL is rejected without any request', async () => {
|
|
71
|
+
await expectReject('/carimage/noimage/7pNvWELuWhZX/large-cached.jpeg', R.placeholder);
|
|
72
|
+
assert.equal(hits['/carimage/noimage/7pNvWELuWhZX/large-cached.jpeg'], undefined);
|
|
73
|
+
}],
|
|
74
|
+
['non-HTTPS, credentials and relative URLs are rejected without a request', async () => {
|
|
75
|
+
const strict = new ImageUrlValidator();
|
|
76
|
+
assert.equal((await strict.validate(`${base}/ok.png`)).reason, R.notHttps);
|
|
77
|
+
assert.equal((await strict.validate('https://user:secret@images.auto.de/carimage/a/b/large-cached.jpeg')).reason, R.credentials);
|
|
78
|
+
assert.equal((await strict.validate('/carimage/a/b/large-cached.jpeg')).reason, R.invalidUrl);
|
|
79
|
+
assert.equal((await strict.validate('ftp://images.auto.de/a.jpg')).reason, R.invalidUrl);
|
|
80
|
+
}],
|
|
81
|
+
['each URL is probed once per run (cached) and counted once', async () => {
|
|
82
|
+
const v = validator();
|
|
83
|
+
const before = hits['/ok.jpg'];
|
|
84
|
+
await v.validateAll([`${base}/ok.jpg`, `${base}/ok.jpg`, `${base}/missing.jpg`]);
|
|
85
|
+
await v.validate(`${base}/ok.jpg`);
|
|
86
|
+
assert.equal(hits['/ok.jpg'] - before, 1);
|
|
87
|
+
assert.deepEqual(v.stats, { ok: 1, [R.httpStatus]: 1 });
|
|
88
|
+
}],
|
|
89
|
+
['probe budget: URLs beyond maxProbes are reported, not fetched', async () => {
|
|
90
|
+
const v = validator({ maxProbes: 1 });
|
|
91
|
+
const [first, second] = await v.validateAll([`${base}/ok.jpg`, `${base}/ok.png`]);
|
|
92
|
+
assert.equal(first.ok, true);
|
|
93
|
+
assert.equal(second.reason, R.budgetExhausted);
|
|
94
|
+
}],
|
|
95
|
+
];
|
|
96
|
+
|
|
97
|
+
let failed = 0;
|
|
98
|
+
for (const [name, fn] of cases) {
|
|
99
|
+
try {
|
|
100
|
+
await fn();
|
|
101
|
+
console.log(`ok ${name}`);
|
|
102
|
+
} catch (e) {
|
|
103
|
+
failed += 1;
|
|
104
|
+
console.error(`FAIL ${name}\n ${e.message}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
(server as any).closeAllConnections?.(); // Node ≥18.2; @types/node here is older
|
|
108
|
+
server.close();
|
|
109
|
+
if (failed) process.exit(1);
|
|
110
|
+
})();
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// Run: npm test (ts-node, no test framework needed)
|
|
2
|
+
// ADV-336 regression lock: the image URLs every existing partner export (Google, Meta-marketing,
|
|
3
|
+
// default partners) builds must stay byte-identical. images.golden.json was recorded from the
|
|
4
|
+
// ImagesService BEFORE the clean profile was added — never regenerate it to make this test pass.
|
|
5
|
+
import * as assert from 'node:assert/strict';
|
|
6
|
+
import { readFileSync } from 'fs';
|
|
7
|
+
import { snapshotExistingImageOutputs } from './fixtures/images-service.harness';
|
|
8
|
+
|
|
9
|
+
const golden = JSON.parse(readFileSync(`${__dirname}/fixtures/images.golden.json`, 'utf8'));
|
|
10
|
+
|
|
11
|
+
snapshotExistingImageOutputs().then((actual) => {
|
|
12
|
+
let failed = 0;
|
|
13
|
+
for (const name of Object.keys(golden)) {
|
|
14
|
+
try {
|
|
15
|
+
assert.deepEqual(actual[name], golden[name]);
|
|
16
|
+
console.log(`ok existing image outputs unchanged: ${name}`);
|
|
17
|
+
} catch (e) {
|
|
18
|
+
failed += 1;
|
|
19
|
+
console.error(`FAIL existing image outputs changed: ${name}\n ${e.message}`);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
if (failed) process.exit(1);
|
|
23
|
+
});
|