@opentermsarchive/engine 11.0.1 → 12.0.0
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/.eslintrc.yaml +3 -0
- package/config/default.json +5 -0
- package/config/test.json +4 -1
- package/package.json +3 -2
- package/scripts/reporter/duplicate/index.js +1 -1
- package/src/archivist/collection/index.test.js +1 -1
- package/src/archivist/recorder/record.js +21 -0
- package/src/archivist/recorder/repositories/git/dataMapper.js +20 -11
- package/src/archivist/recorder/repositories/git/git.js +14 -2
- package/src/archivist/recorder/repositories/git/index.js +67 -11
- package/src/archivist/recorder/repositories/git/index.test.js +282 -6
- package/src/archivist/recorder/repositories/interface.js +44 -6
- package/src/archivist/recorder/repositories/mongo/index.js +67 -4
- package/src/archivist/recorder/repositories/mongo/index.test.js +330 -13
- package/src/archivist/recorder/version.test.js +38 -0
- package/src/archivist/services/index.js +1 -1
- package/src/archivist/services/sourceDocument.js +19 -1
- package/src/archivist/services/sourceDocument.test.js +57 -0
- package/src/collection-api/routes/feed.js +253 -0
- package/src/collection-api/routes/feed.test.js +739 -0
- package/src/collection-api/routes/index.js +16 -1
- package/src/collection-api/routes/services.js +1 -2
- package/src/collection-api/routes/services.test.js +4 -41
- package/src/collection-api/routes/versions.js +78 -78
- package/src/collection-api/routes/versions.test.js +4 -4
- package/src/collection-api/server.js +2 -0
- package/src/reporter/gitlab/index.js +1 -1
|
@@ -88,13 +88,76 @@ export default class MongoRepository extends RepositoryInterface {
|
|
|
88
88
|
return this.#toDomain(mongoDocument);
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
-
async findAll() {
|
|
92
|
-
|
|
91
|
+
async findAll({ limit, offset, includeTechnicalUpgrades = true } = {}) {
|
|
92
|
+
const filter = includeTechnicalUpgrades ? {} : { isTechnicalUpgrade: { $ne: true } };
|
|
93
|
+
let query = this.collection.find(filter).project({ content: 0 }).sort({ fetchDate: -1 });
|
|
94
|
+
|
|
95
|
+
if (offset !== undefined) {
|
|
96
|
+
query = query.skip(offset);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (limit !== undefined) {
|
|
100
|
+
query = query.limit(limit);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return Promise.all((await query.toArray())
|
|
104
|
+
.map(mongoDocument => this.#toDomain(mongoDocument, { deferContentLoading: true })));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async findByServiceAndTermsType(serviceId, termsType, { limit, offset, includeTechnicalUpgrades = true } = {}) {
|
|
108
|
+
const filter = { serviceId, termsType };
|
|
109
|
+
|
|
110
|
+
if (!includeTechnicalUpgrades) {
|
|
111
|
+
filter.isTechnicalUpgrade = { $ne: true };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
let query = this.collection.find(filter).project({ content: 0 }).sort({ fetchDate: -1 });
|
|
115
|
+
|
|
116
|
+
if (offset !== undefined) {
|
|
117
|
+
query = query.skip(offset);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (limit !== undefined) {
|
|
121
|
+
query = query.limit(limit);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return Promise.all((await query.toArray())
|
|
93
125
|
.map(mongoDocument => this.#toDomain(mongoDocument, { deferContentLoading: true })));
|
|
94
126
|
}
|
|
95
127
|
|
|
96
|
-
|
|
97
|
-
|
|
128
|
+
async findByService(serviceId, { limit, offset, includeTechnicalUpgrades = true } = {}) {
|
|
129
|
+
const filter = { serviceId };
|
|
130
|
+
|
|
131
|
+
if (!includeTechnicalUpgrades) {
|
|
132
|
+
filter.isTechnicalUpgrade = { $ne: true };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
let query = this.collection.find(filter).project({ content: 0 }).sort({ fetchDate: -1 });
|
|
136
|
+
|
|
137
|
+
if (offset !== undefined) {
|
|
138
|
+
query = query.skip(offset);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (limit !== undefined) {
|
|
142
|
+
query = query.limit(limit);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return Promise.all((await query.toArray())
|
|
146
|
+
.map(mongoDocument => this.#toDomain(mongoDocument, { deferContentLoading: true })));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
count(serviceId, termsType) {
|
|
150
|
+
const filter = {};
|
|
151
|
+
|
|
152
|
+
if (serviceId) {
|
|
153
|
+
filter.serviceId = serviceId;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (termsType) {
|
|
157
|
+
filter.termsType = termsType;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return this.collection.countDocuments(filter);
|
|
98
161
|
}
|
|
99
162
|
|
|
100
163
|
async* iterate() {
|
|
@@ -629,45 +629,362 @@ describe('MongoRepository', () => {
|
|
|
629
629
|
}
|
|
630
630
|
});
|
|
631
631
|
|
|
632
|
-
it('returns records in
|
|
633
|
-
expect(records.map(record => record.fetchDate)).to.deep.equal([
|
|
632
|
+
it('returns records in descending order', () => {
|
|
633
|
+
expect(records.map(record => record.fetchDate)).to.deep.equal([ FETCH_DATE_LATER, FETCH_DATE, FETCH_DATE_EARLIER ]);
|
|
634
|
+
});
|
|
635
|
+
|
|
636
|
+
context('with includeTechnicalUpgrades: false', () => {
|
|
637
|
+
let filteredRecords;
|
|
638
|
+
|
|
639
|
+
before(async () => {
|
|
640
|
+
filteredRecords = await subject.findAll({ includeTechnicalUpgrades: false });
|
|
641
|
+
});
|
|
642
|
+
|
|
643
|
+
it('excludes technical upgrade records', () => {
|
|
644
|
+
expect(filteredRecords.length).to.equal(2);
|
|
645
|
+
});
|
|
646
|
+
|
|
647
|
+
it('only returns records that represent actual content changes', () => {
|
|
648
|
+
for (const record of filteredRecords) {
|
|
649
|
+
expect(record.isTechnicalUpgrade).to.not.be.true;
|
|
650
|
+
}
|
|
651
|
+
});
|
|
652
|
+
|
|
653
|
+
it('returns the expected records in descending order', () => {
|
|
654
|
+
expect(filteredRecords.map(record => record.fetchDate)).to.deep.equal([ FETCH_DATE_LATER, FETCH_DATE ]);
|
|
655
|
+
});
|
|
634
656
|
});
|
|
635
657
|
});
|
|
636
658
|
|
|
637
|
-
describe('#
|
|
638
|
-
|
|
659
|
+
describe('#findByServiceAndTermsType', () => {
|
|
660
|
+
const expectedIds = [];
|
|
661
|
+
let records;
|
|
639
662
|
|
|
640
663
|
before(async () => {
|
|
641
|
-
await subject.save(new Version({
|
|
664
|
+
const { id: id1 } = await subject.save(new Version({
|
|
642
665
|
serviceId: SERVICE_PROVIDER_ID,
|
|
643
666
|
termsType: TERMS_TYPE,
|
|
644
667
|
content: CONTENT,
|
|
645
668
|
fetchDate: FETCH_DATE,
|
|
646
669
|
snapshotIds: [SNAPSHOT_ID],
|
|
647
670
|
}));
|
|
648
|
-
|
|
671
|
+
|
|
672
|
+
expectedIds.push(id1);
|
|
673
|
+
|
|
674
|
+
const { id: id2 } = await subject.save(new Version({
|
|
649
675
|
serviceId: SERVICE_PROVIDER_ID,
|
|
650
676
|
termsType: TERMS_TYPE,
|
|
651
677
|
content: `${CONTENT} - updated`,
|
|
652
678
|
fetchDate: FETCH_DATE_LATER,
|
|
653
679
|
snapshotIds: [SNAPSHOT_ID],
|
|
654
680
|
}));
|
|
681
|
+
|
|
682
|
+
expectedIds.push(id2);
|
|
683
|
+
|
|
655
684
|
await subject.save(new Version({
|
|
685
|
+
serviceId: 'other_service',
|
|
686
|
+
termsType: 'Privacy Policy',
|
|
687
|
+
content: `${CONTENT} - other`,
|
|
688
|
+
fetchDate: FETCH_DATE,
|
|
689
|
+
snapshotIds: [SNAPSHOT_ID],
|
|
690
|
+
}));
|
|
691
|
+
|
|
692
|
+
(records = await subject.findByServiceAndTermsType(SERVICE_PROVIDER_ID, TERMS_TYPE));
|
|
693
|
+
});
|
|
694
|
+
|
|
695
|
+
after(() => subject.removeAll());
|
|
696
|
+
|
|
697
|
+
it('returns only matching records', () => {
|
|
698
|
+
expect(records.length).to.equal(2);
|
|
699
|
+
});
|
|
700
|
+
|
|
701
|
+
it('returns Version objects', () => {
|
|
702
|
+
for (const record of records) {
|
|
703
|
+
expect(record).to.be.an.instanceof(Version);
|
|
704
|
+
}
|
|
705
|
+
});
|
|
706
|
+
|
|
707
|
+
it('returns records with matching service ID', () => {
|
|
708
|
+
for (const record of records) {
|
|
709
|
+
expect(record.serviceId).to.equal(SERVICE_PROVIDER_ID);
|
|
710
|
+
}
|
|
711
|
+
});
|
|
712
|
+
|
|
713
|
+
it('returns records with matching terms type', () => {
|
|
714
|
+
for (const record of records) {
|
|
715
|
+
expect(record.termsType).to.equal(TERMS_TYPE);
|
|
716
|
+
}
|
|
717
|
+
});
|
|
718
|
+
|
|
719
|
+
it('returns records in descending order', () => {
|
|
720
|
+
expect(records.map(record => record.fetchDate)).to.deep.equal([ FETCH_DATE_LATER, FETCH_DATE ]);
|
|
721
|
+
});
|
|
722
|
+
|
|
723
|
+
it('returns records with correct IDs', () => {
|
|
724
|
+
expect(records.map(record => record.id)).to.have.members(expectedIds);
|
|
725
|
+
});
|
|
726
|
+
|
|
727
|
+
context('when no matching records exist', () => {
|
|
728
|
+
it('returns an empty array', async () => {
|
|
729
|
+
const result = await subject.findByServiceAndTermsType('non_existent_service', 'Non Existent Terms');
|
|
730
|
+
|
|
731
|
+
expect(result).to.be.an('array').that.is.empty;
|
|
732
|
+
});
|
|
733
|
+
});
|
|
734
|
+
|
|
735
|
+
context('with includeTechnicalUpgrades: false', () => {
|
|
736
|
+
let filteredRecords;
|
|
737
|
+
let technicalUpgradeId;
|
|
738
|
+
|
|
739
|
+
before(async () => {
|
|
740
|
+
({ id: technicalUpgradeId } = await subject.save(new Version({
|
|
741
|
+
serviceId: SERVICE_PROVIDER_ID,
|
|
742
|
+
termsType: TERMS_TYPE,
|
|
743
|
+
content: `${CONTENT} - technical upgrade`,
|
|
744
|
+
fetchDate: FETCH_DATE_EARLIER,
|
|
745
|
+
snapshotIds: [SNAPSHOT_ID],
|
|
746
|
+
isTechnicalUpgrade: true,
|
|
747
|
+
})));
|
|
748
|
+
|
|
749
|
+
filteredRecords = await subject.findByServiceAndTermsType(SERVICE_PROVIDER_ID, TERMS_TYPE, { includeTechnicalUpgrades: false });
|
|
750
|
+
});
|
|
751
|
+
|
|
752
|
+
it('excludes technical upgrade records', () => {
|
|
753
|
+
expect(filteredRecords.map(record => record.id)).to.not.include(technicalUpgradeId);
|
|
754
|
+
});
|
|
755
|
+
|
|
756
|
+
it('only returns records that represent actual content changes', () => {
|
|
757
|
+
for (const record of filteredRecords) {
|
|
758
|
+
expect(record.isTechnicalUpgrade).to.not.be.true;
|
|
759
|
+
}
|
|
760
|
+
});
|
|
761
|
+
});
|
|
762
|
+
});
|
|
763
|
+
|
|
764
|
+
describe('#findByService', () => {
|
|
765
|
+
const OTHER_TERMS_TYPE = 'Privacy Policy';
|
|
766
|
+
const expectedIds = [];
|
|
767
|
+
let records;
|
|
768
|
+
|
|
769
|
+
before(async () => {
|
|
770
|
+
const { id: id1 } = await subject.save(new Version({
|
|
656
771
|
serviceId: SERVICE_PROVIDER_ID,
|
|
657
772
|
termsType: TERMS_TYPE,
|
|
658
|
-
content:
|
|
659
|
-
|
|
773
|
+
content: CONTENT,
|
|
774
|
+
fetchDate: FETCH_DATE,
|
|
775
|
+
snapshotIds: [SNAPSHOT_ID],
|
|
776
|
+
}));
|
|
777
|
+
|
|
778
|
+
expectedIds.push(id1);
|
|
779
|
+
|
|
780
|
+
const { id: id2 } = await subject.save(new Version({
|
|
781
|
+
serviceId: SERVICE_PROVIDER_ID,
|
|
782
|
+
termsType: TERMS_TYPE,
|
|
783
|
+
content: `${CONTENT} - updated`,
|
|
784
|
+
fetchDate: FETCH_DATE_LATER,
|
|
785
|
+
snapshotIds: [SNAPSHOT_ID],
|
|
786
|
+
}));
|
|
787
|
+
|
|
788
|
+
expectedIds.push(id2);
|
|
789
|
+
|
|
790
|
+
const { id: id3 } = await subject.save(new Version({
|
|
791
|
+
serviceId: SERVICE_PROVIDER_ID,
|
|
792
|
+
termsType: OTHER_TERMS_TYPE,
|
|
793
|
+
content: `${CONTENT} - other terms type`,
|
|
660
794
|
fetchDate: FETCH_DATE_EARLIER,
|
|
661
795
|
snapshotIds: [SNAPSHOT_ID],
|
|
662
796
|
}));
|
|
663
797
|
|
|
664
|
-
|
|
798
|
+
expectedIds.push(id3);
|
|
799
|
+
|
|
800
|
+
await subject.save(new Version({
|
|
801
|
+
serviceId: 'other_service',
|
|
802
|
+
termsType: TERMS_TYPE,
|
|
803
|
+
content: `${CONTENT} - other service`,
|
|
804
|
+
fetchDate: FETCH_DATE,
|
|
805
|
+
snapshotIds: [SNAPSHOT_ID],
|
|
806
|
+
}));
|
|
807
|
+
|
|
808
|
+
(records = await subject.findByService(SERVICE_PROVIDER_ID));
|
|
665
809
|
});
|
|
666
810
|
|
|
667
811
|
after(() => subject.removeAll());
|
|
668
812
|
|
|
669
|
-
it('returns
|
|
670
|
-
expect(
|
|
813
|
+
it('returns only matching records', () => {
|
|
814
|
+
expect(records.length).to.equal(3);
|
|
815
|
+
});
|
|
816
|
+
|
|
817
|
+
it('returns Version objects', () => {
|
|
818
|
+
for (const record of records) {
|
|
819
|
+
expect(record).to.be.an.instanceof(Version);
|
|
820
|
+
}
|
|
821
|
+
});
|
|
822
|
+
|
|
823
|
+
it('returns records with matching service ID', () => {
|
|
824
|
+
for (const record of records) {
|
|
825
|
+
expect(record.serviceId).to.equal(SERVICE_PROVIDER_ID);
|
|
826
|
+
}
|
|
827
|
+
});
|
|
828
|
+
|
|
829
|
+
it('returns records across all terms types of the service', () => {
|
|
830
|
+
expect(new Set(records.map(record => record.termsType))).to.deep.equal(new Set([ TERMS_TYPE, OTHER_TERMS_TYPE ]));
|
|
831
|
+
});
|
|
832
|
+
|
|
833
|
+
it('returns records in descending order', () => {
|
|
834
|
+
expect(records.map(record => record.fetchDate)).to.deep.equal([ FETCH_DATE_LATER, FETCH_DATE, FETCH_DATE_EARLIER ]);
|
|
835
|
+
});
|
|
836
|
+
|
|
837
|
+
it('returns records with correct IDs', () => {
|
|
838
|
+
expect(records.map(record => record.id)).to.have.members(expectedIds);
|
|
839
|
+
});
|
|
840
|
+
|
|
841
|
+
context('when no matching records exist', () => {
|
|
842
|
+
it('returns an empty array', async () => {
|
|
843
|
+
const result = await subject.findByService('non_existent_service');
|
|
844
|
+
|
|
845
|
+
expect(result).to.be.an('array').that.is.empty;
|
|
846
|
+
});
|
|
847
|
+
});
|
|
848
|
+
|
|
849
|
+
context('with includeTechnicalUpgrades: false', () => {
|
|
850
|
+
let filteredRecords;
|
|
851
|
+
let technicalUpgradeId;
|
|
852
|
+
|
|
853
|
+
before(async () => {
|
|
854
|
+
({ id: technicalUpgradeId } = await subject.save(new Version({
|
|
855
|
+
serviceId: SERVICE_PROVIDER_ID,
|
|
856
|
+
termsType: TERMS_TYPE,
|
|
857
|
+
content: `${CONTENT} - technical upgrade`,
|
|
858
|
+
fetchDate: new Date('2000-01-03T12:00:00.000Z'),
|
|
859
|
+
snapshotIds: [SNAPSHOT_ID],
|
|
860
|
+
isTechnicalUpgrade: true,
|
|
861
|
+
})));
|
|
862
|
+
|
|
863
|
+
filteredRecords = await subject.findByService(SERVICE_PROVIDER_ID, { includeTechnicalUpgrades: false });
|
|
864
|
+
});
|
|
865
|
+
|
|
866
|
+
it('excludes technical upgrade records', () => {
|
|
867
|
+
expect(filteredRecords.map(record => record.id)).to.not.include(technicalUpgradeId);
|
|
868
|
+
});
|
|
869
|
+
|
|
870
|
+
it('only returns records that represent actual content changes', () => {
|
|
871
|
+
for (const record of filteredRecords) {
|
|
872
|
+
expect(record.isTechnicalUpgrade).to.not.be.true;
|
|
873
|
+
}
|
|
874
|
+
});
|
|
875
|
+
});
|
|
876
|
+
});
|
|
877
|
+
|
|
878
|
+
describe('#count', () => {
|
|
879
|
+
context('without filters', () => {
|
|
880
|
+
let count;
|
|
881
|
+
|
|
882
|
+
before(async () => {
|
|
883
|
+
await subject.save(new Version({
|
|
884
|
+
serviceId: SERVICE_PROVIDER_ID,
|
|
885
|
+
termsType: TERMS_TYPE,
|
|
886
|
+
content: CONTENT,
|
|
887
|
+
fetchDate: FETCH_DATE,
|
|
888
|
+
snapshotIds: [SNAPSHOT_ID],
|
|
889
|
+
}));
|
|
890
|
+
await subject.save(new Version({
|
|
891
|
+
serviceId: SERVICE_PROVIDER_ID,
|
|
892
|
+
termsType: TERMS_TYPE,
|
|
893
|
+
content: `${CONTENT} - updated`,
|
|
894
|
+
fetchDate: FETCH_DATE_LATER,
|
|
895
|
+
snapshotIds: [SNAPSHOT_ID],
|
|
896
|
+
}));
|
|
897
|
+
await subject.save(new Version({
|
|
898
|
+
serviceId: SERVICE_PROVIDER_ID,
|
|
899
|
+
termsType: TERMS_TYPE,
|
|
900
|
+
content: `${CONTENT} - updated 2`,
|
|
901
|
+
isTechnicalUpgrade: true,
|
|
902
|
+
fetchDate: FETCH_DATE_EARLIER,
|
|
903
|
+
snapshotIds: [SNAPSHOT_ID],
|
|
904
|
+
}));
|
|
905
|
+
|
|
906
|
+
(count = await subject.count());
|
|
907
|
+
});
|
|
908
|
+
|
|
909
|
+
after(() => subject.removeAll());
|
|
910
|
+
|
|
911
|
+
it('returns the proper count', () => {
|
|
912
|
+
expect(count).to.equal(3);
|
|
913
|
+
});
|
|
914
|
+
});
|
|
915
|
+
|
|
916
|
+
context('with serviceId and termsType filters', () => {
|
|
917
|
+
before(async () => {
|
|
918
|
+
await subject.save(new Version({
|
|
919
|
+
serviceId: SERVICE_PROVIDER_ID,
|
|
920
|
+
termsType: TERMS_TYPE,
|
|
921
|
+
content: CONTENT,
|
|
922
|
+
fetchDate: FETCH_DATE,
|
|
923
|
+
snapshotIds: [SNAPSHOT_ID],
|
|
924
|
+
}));
|
|
925
|
+
await subject.save(new Version({
|
|
926
|
+
serviceId: SERVICE_PROVIDER_ID,
|
|
927
|
+
termsType: TERMS_TYPE,
|
|
928
|
+
content: `${CONTENT} - updated`,
|
|
929
|
+
fetchDate: FETCH_DATE_LATER,
|
|
930
|
+
snapshotIds: [SNAPSHOT_ID],
|
|
931
|
+
}));
|
|
932
|
+
await subject.save(new Version({
|
|
933
|
+
serviceId: 'other_service',
|
|
934
|
+
termsType: 'Privacy Policy',
|
|
935
|
+
content: 'Other content',
|
|
936
|
+
fetchDate: FETCH_DATE,
|
|
937
|
+
snapshotIds: [SNAPSHOT_ID],
|
|
938
|
+
}));
|
|
939
|
+
});
|
|
940
|
+
|
|
941
|
+
after(() => subject.removeAll());
|
|
942
|
+
|
|
943
|
+
it('returns count for specific service and terms type', async () => {
|
|
944
|
+
const filteredCount = await subject.count(SERVICE_PROVIDER_ID, TERMS_TYPE);
|
|
945
|
+
|
|
946
|
+
expect(filteredCount).to.equal(2);
|
|
947
|
+
});
|
|
948
|
+
|
|
949
|
+
it('returns zero for non-existent service', async () => {
|
|
950
|
+
const filteredCount = await subject.count('non-existent-service', TERMS_TYPE);
|
|
951
|
+
|
|
952
|
+
expect(filteredCount).to.equal(0);
|
|
953
|
+
});
|
|
954
|
+
});
|
|
955
|
+
|
|
956
|
+
context('with only serviceId filter', () => {
|
|
957
|
+
before(async () => {
|
|
958
|
+
await subject.save(new Version({
|
|
959
|
+
serviceId: SERVICE_PROVIDER_ID,
|
|
960
|
+
termsType: TERMS_TYPE,
|
|
961
|
+
content: CONTENT,
|
|
962
|
+
fetchDate: FETCH_DATE,
|
|
963
|
+
snapshotIds: [SNAPSHOT_ID],
|
|
964
|
+
}));
|
|
965
|
+
await subject.save(new Version({
|
|
966
|
+
serviceId: SERVICE_PROVIDER_ID,
|
|
967
|
+
termsType: 'Different Terms',
|
|
968
|
+
content: 'Different content',
|
|
969
|
+
fetchDate: FETCH_DATE_LATER,
|
|
970
|
+
snapshotIds: [SNAPSHOT_ID],
|
|
971
|
+
}));
|
|
972
|
+
await subject.save(new Version({
|
|
973
|
+
serviceId: 'other_service',
|
|
974
|
+
termsType: 'Privacy Policy',
|
|
975
|
+
content: 'Other content',
|
|
976
|
+
fetchDate: FETCH_DATE,
|
|
977
|
+
snapshotIds: [SNAPSHOT_ID],
|
|
978
|
+
}));
|
|
979
|
+
});
|
|
980
|
+
|
|
981
|
+
after(() => subject.removeAll());
|
|
982
|
+
|
|
983
|
+
it('returns count for all terms types of a service', async () => {
|
|
984
|
+
const filteredCount = await subject.count(SERVICE_PROVIDER_ID);
|
|
985
|
+
|
|
986
|
+
expect(filteredCount).to.equal(2);
|
|
987
|
+
});
|
|
671
988
|
});
|
|
672
989
|
});
|
|
673
990
|
|
|
@@ -1197,8 +1514,8 @@ describe('MongoRepository', () => {
|
|
|
1197
1514
|
}
|
|
1198
1515
|
});
|
|
1199
1516
|
|
|
1200
|
-
it('returns records in
|
|
1201
|
-
expect(records.map(record => record.fetchDate)).to.deep.equal([
|
|
1517
|
+
it('returns records in descending order', () => {
|
|
1518
|
+
expect(records.map(record => record.fetchDate)).to.deep.equal([ FETCH_DATE_LATER, FETCH_DATE, FETCH_DATE_EARLIER ]);
|
|
1202
1519
|
});
|
|
1203
1520
|
});
|
|
1204
1521
|
|
|
@@ -60,4 +60,42 @@ describe('Version', () => {
|
|
|
60
60
|
});
|
|
61
61
|
});
|
|
62
62
|
});
|
|
63
|
+
|
|
64
|
+
describe('#displayTitle', () => {
|
|
65
|
+
const baseParams = {
|
|
66
|
+
serviceId: 'service-A',
|
|
67
|
+
termsType: 'Terms of Service',
|
|
68
|
+
fetchDate: new Date('2000-01-01T12:00:00.000Z'),
|
|
69
|
+
content: 'some content',
|
|
70
|
+
snapshotIds: ['dd263f270b3065e1c18201b49ab898474b357566'],
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
context('when the record is the first one for its service and terms type', () => {
|
|
74
|
+
it('starts with the first-record prefix', () => {
|
|
75
|
+
subject = new Version({ ...baseParams, isFirstRecord: true });
|
|
76
|
+
expect(subject.displayTitle).to.equal('First record of service-A Terms of Service');
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
context('when the record is a technical upgrade', () => {
|
|
81
|
+
it('starts with the technical-upgrade prefix', () => {
|
|
82
|
+
subject = new Version({ ...baseParams, isTechnicalUpgrade: true });
|
|
83
|
+
expect(subject.displayTitle).to.equal('Apply technical or declaration upgrade on service-A Terms of Service');
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
context('when the record is a regular content change', () => {
|
|
88
|
+
it('starts with the update prefix', () => {
|
|
89
|
+
subject = new Version(baseParams);
|
|
90
|
+
expect(subject.displayTitle).to.equal('Record new changes of service-A Terms of Service');
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
context('when the record is both a first record and a technical upgrade', () => {
|
|
95
|
+
it('prioritises the first-record prefix', () => {
|
|
96
|
+
subject = new Version({ ...baseParams, isFirstRecord: true, isTechnicalUpgrade: true });
|
|
97
|
+
expect(subject.displayTitle).to.equal('First record of service-A Terms of Service');
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
});
|
|
63
101
|
});
|
|
@@ -281,7 +281,7 @@ function getHistoryFilePaths(serviceId) {
|
|
|
281
281
|
}
|
|
282
282
|
|
|
283
283
|
async function loadServiceHistory(historyFilePath) {
|
|
284
|
-
if (!(await fileExists(historyFilePath))) return {};
|
|
284
|
+
if (!(await fileExists(historyFilePath))) { return {}; }
|
|
285
285
|
|
|
286
286
|
try {
|
|
287
287
|
return JSON.parse(await fs.readFile(historyFilePath));
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
|
|
3
|
+
import mime from 'mime';
|
|
4
|
+
|
|
1
5
|
export default class SourceDocument {
|
|
2
6
|
/**
|
|
3
7
|
* Represents a source document containing web content and metadata for extraction.
|
|
@@ -21,7 +25,7 @@ export default class SourceDocument {
|
|
|
21
25
|
this.filters = filters;
|
|
22
26
|
this.content = content;
|
|
23
27
|
this.mimeType = mimeType;
|
|
24
|
-
this.id =
|
|
28
|
+
this.id = SourceDocument.generateId(location);
|
|
25
29
|
}
|
|
26
30
|
|
|
27
31
|
get cssSelectors() {
|
|
@@ -60,6 +64,20 @@ export default class SourceDocument {
|
|
|
60
64
|
return [selector];
|
|
61
65
|
}
|
|
62
66
|
|
|
67
|
+
static generateId(location) {
|
|
68
|
+
const ILLEGAL_CHARACTERS = /[\\:"<>|*?]/g; // Characters forbidden in filenames for cross-platform compatibility; see https://github.com/actions/toolkit/blob/main/packages/artifact/src/internal/upload/path-and-artifact-name-validation.ts
|
|
69
|
+
|
|
70
|
+
const pathname = decodeURIComponent(new URL(location).pathname);
|
|
71
|
+
const extension = path.extname(pathname);
|
|
72
|
+
const pathnameWithoutExtension = mime.getType(extension) ? pathname.slice(0, -extension.length) : pathname; // Remove file extension when it corresponds to a known MIME type, as the extension is not part of the document's identity but a web server implementation detail
|
|
73
|
+
|
|
74
|
+
return pathnameWithoutExtension
|
|
75
|
+
.split('/')
|
|
76
|
+
.filter(Boolean)
|
|
77
|
+
.join('-')
|
|
78
|
+
.replace(ILLEGAL_CHARACTERS, '_');
|
|
79
|
+
}
|
|
80
|
+
|
|
63
81
|
toPersistence() {
|
|
64
82
|
return {
|
|
65
83
|
fetch: this.location,
|
|
@@ -157,6 +157,63 @@ describe('SourceDocument', () => {
|
|
|
157
157
|
});
|
|
158
158
|
});
|
|
159
159
|
|
|
160
|
+
describe('#generateId', () => {
|
|
161
|
+
it('generates ID from URL pathname', () => {
|
|
162
|
+
expect(new SourceDocument({ location: 'https://example.com/legal/terms' }).id).to.equal('legal-terms');
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('returns empty string for root URL', () => {
|
|
166
|
+
expect(new SourceDocument({ location: 'https://example.com/' }).id).to.equal('');
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('decodes URL-encoded characters', () => {
|
|
170
|
+
expect(new SourceDocument({ location: 'https://example.com/terms%20of%20service' }).id).to.equal('terms of service');
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('removes known file extension from URL pathname', () => {
|
|
174
|
+
expect(new SourceDocument({ location: 'https://example.com/en.html' }).id).to.equal('en');
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('removes only the last file extension', () => {
|
|
178
|
+
expect(new SourceDocument({ location: 'https://example.com/terms.backup.html' }).id).to.equal('terms.backup');
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('keeps unknown extension in URL pathname', () => {
|
|
182
|
+
expect(new SourceDocument({ location: 'https://example.com/terms.of.service' }).id).to.equal('terms.of.service');
|
|
183
|
+
});
|
|
184
|
+
it('decodes URL-encoded characters before replacing illegal ones', () => {
|
|
185
|
+
expect(new SourceDocument({ location: 'https://example.com/terms%3Aof%3Aservice' }).id).to.equal('terms_of_service');
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
context('replaces characters that are illegal in filenames for cross-platform compatibility', () => {
|
|
189
|
+
const ILLEGAL_CHARACTERS_IN_URL_PATHNAME = {
|
|
190
|
+
':': 'colon',
|
|
191
|
+
'"': 'double quote',
|
|
192
|
+
'<': 'less than',
|
|
193
|
+
'>': 'greater than',
|
|
194
|
+
'|': 'vertical bar',
|
|
195
|
+
'*': 'asterisk',
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
for (const [ character, name ] of Object.entries(ILLEGAL_CHARACTERS_IN_URL_PATHNAME)) {
|
|
199
|
+
it(`replaces ${name} "${character}"`, () => {
|
|
200
|
+
expect(new SourceDocument({ location: `https://example.com/before${character}after` }).id).to.equal('before_after');
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const ILLEGAL_CHARACTERS_ENCODED_IN_URL = {
|
|
205
|
+
'%5C': 'backslash',
|
|
206
|
+
'%3F': 'question mark',
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
for (const [ encoded, name ] of Object.entries(ILLEGAL_CHARACTERS_ENCODED_IN_URL)) {
|
|
210
|
+
it(`replaces ${name} decoded from "${encoded}"`, () => {
|
|
211
|
+
expect(new SourceDocument({ location: `https://example.com/before${encoded}after` }).id).to.equal('before_after');
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
|
|
160
217
|
describe('#toPersistence', () => {
|
|
161
218
|
it('converts basic source document declarations into JSON representation', () => {
|
|
162
219
|
const result = new SourceDocument({
|