3131use OCP \AppFramework \Utility \ITimeFactory ;
3232use OCP \BackgroundJob \IJobList ;
3333use OCP \DB \IPreparedStatement ;
34+ use OCP \DB \QueryBuilder \IQueryBuilder ;
3435use OCP \IConfig ;
3536use OCP \IDBConnection ;
3637use OCP \Server ;
@@ -51,31 +52,11 @@ class DataDeleteActivitiesTest extends TestCase {
5152 protected function setUp (): void {
5253 parent ::setUp ();
5354
54- $ activities = [
55- ['affectedUser ' => 'delete ' , 'subject ' => 'subject ' , 'time ' => 0 ],
56- ['affectedUser ' => 'delete ' , 'subject ' => 'subject2 ' , 'time ' => time () - 2 * 365 * 24 * 3600 ],
57- ['affectedUser ' => 'otherUser ' , 'subject ' => 'subject ' , 'time ' => time ()],
58- ['affectedUser ' => 'otherUser ' , 'subject ' => 'subject2 ' , 'time ' => time ()],
59- ];
55+ $ this ->insertActivity ('delete ' , 0 , 'subject ' );
56+ $ this ->insertActivity ('delete ' , time () - 2 * 365 * 24 * 3600 , 'subject2 ' );
57+ $ this ->insertActivity ('otherUser ' , time (), 'subject ' );
58+ $ this ->insertActivity ('otherUser ' , time (), 'subject2 ' );
6059
61- $ queryActivity = Server::get (IDBConnection::class)
62- ->prepare ('INSERT INTO `*PREFIX*activity`(`app`, `subject`, `subjectparams`, `message`, `messageparams`, `file`, `link`, `user`, `affecteduser`, `timestamp`, `priority`, `type`) ' . ' VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? ) ' );
63- foreach ($ activities as $ activity ) {
64- $ queryActivity ->execute ([
65- 'app ' ,
66- $ activity ['subject ' ],
67- json_encode ([]),
68- '' ,
69- json_encode ([]),
70- 'file ' ,
71- 'link ' ,
72- 'user ' ,
73- $ activity ['affectedUser ' ],
74- $ activity ['time ' ],
75- IExtension::PRIORITY_MEDIUM ,
76- 'test ' ,
77- ]);
78- }
7960 $ this ->data = new Data (
8061 $ this ->createMock (IManager::class),
8162 Server::get (IDBConnection::class),
@@ -84,20 +65,40 @@ protected function setUp(): void {
8465 );
8566 }
8667
68+ private function insertActivity (string $ affectedUser , int $ time , string $ subject = 'subject ' ): void {
69+ $ query = Server::get (IDBConnection::class)->getQueryBuilder ();
70+ $ query ->insert ('activity ' )
71+ ->values ([
72+ 'app ' => $ query ->createNamedParameter ('app ' ),
73+ 'subject ' => $ query ->createNamedParameter ($ subject ),
74+ 'subjectparams ' => $ query ->createNamedParameter (json_encode ([])),
75+ 'message ' => $ query ->createNamedParameter ('' ),
76+ 'messageparams ' => $ query ->createNamedParameter (json_encode ([])),
77+ 'file ' => $ query ->createNamedParameter ('file ' ),
78+ 'link ' => $ query ->createNamedParameter ('link ' ),
79+ 'user ' => $ query ->createNamedParameter ('user ' ),
80+ 'affecteduser ' => $ query ->createNamedParameter ($ affectedUser ),
81+ 'timestamp ' => $ query ->createNamedParameter ($ time , IQueryBuilder::PARAM_INT ),
82+ 'priority ' => $ query ->createNamedParameter (IExtension::PRIORITY_MEDIUM , IQueryBuilder::PARAM_INT ),
83+ 'type ' => $ query ->createNamedParameter ('test ' ),
84+ ]);
85+ $ query ->executeStatement ();
86+ }
87+
8788 protected function tearDown (): void {
8889 $ this ->data ->deleteActivities ([
89- 'type ' => 'test ' ,
90+ [ 'type ' , 'test ' ] ,
9091 ]);
9192
9293 parent ::tearDown ();
9394 }
9495
9596 public static function deleteActivitiesData (): array {
9697 return [
97- [['affecteduser ' => 'delete ' ], ['otherUser ' ]],
98- [['affecteduser ' => [ 'delete ' , '= ' ]], ['otherUser ' ]],
99- [['timestamp ' => [ time () - 10 , '< ' ]], ['otherUser ' ]],
100- [['timestamp ' => [ time () - 10 , '> ' ]], ['delete ' ]],
98+ [[[ 'affecteduser ' , 'delete ' ] ], ['otherUser ' ]],
99+ [[[ 'affecteduser ' , 'delete ' , '= ' ]], ['otherUser ' ]],
100+ [[[ 'timestamp ' , time () - 10 , '< ' ]], ['otherUser ' ]],
101+ [[[ 'timestamp ' , time () - 10 , '> ' ]], ['delete ' ]],
101102 ];
102103 }
103104
@@ -125,6 +126,50 @@ public function testExpireActivities(): void {
125126 $ this ->assertUserActivities (['otherUser ' ]);
126127 }
127128
129+ /**
130+ * Regression test for https://github.com/nextcloud/activity/issues/2647
131+ *
132+ * With 'activity_expire_exclude_users' set, expire() builds more than one
133+ * condition. This must:
134+ * - not crash (the excluded users used to be added in a shape the delete
135+ * query could not parse), and
136+ * - keep ANDing every condition together. On MySQL/MariaDB the chunked
137+ * delete used where() in a loop, which dropped all but the last condition
138+ * and would have deleted recent, non-expired activity.
139+ */
140+ public function testExpireActivitiesWithExcludedUsers (): void {
141+ // Old activity for a user that is NOT excluded -> must still be deleted.
142+ $ this ->insertActivity ('expired ' , 0 , 'subject ' );
143+
144+ $ config = $ this ->createMock (IConfig::class);
145+ $ config ->method ('getSystemValue ' )
146+ ->willReturnCallback (static function ($ key , $ default ) {
147+ if ($ key === 'activity_expire_exclude_users ' ) {
148+ return ['delete ' , 'otherUser ' ];
149+ }
150+ return $ default ;
151+ });
152+ $ data = new Data (
153+ $ this ->createMock (IManager::class),
154+ Server::get (IDBConnection::class),
155+ $ this ->createMock (LoggerInterface::class),
156+ $ config ,
157+ );
158+
159+ $ time = $ this ->createMock (ITimeFactory::class);
160+ $ time ->method ('getTime ' )
161+ ->willReturn (time ());
162+ $ backgroundjob = new ExpireActivities ($ time , $ data , $ config );
163+ $ backgroundjob ->setId ('1 ' );
164+
165+ $ this ->assertUserActivities (['delete ' , 'expired ' , 'otherUser ' ]);
166+ $ backgroundjob ->start ($ this ->createMock (IJobList::class));
167+
168+ // 'delete' (old) survives because it is excluded; 'otherUser' (recent)
169+ // survives; 'expired' (old, not excluded) is the only one deleted.
170+ $ this ->assertUserActivities (['delete ' , 'otherUser ' ]);
171+ }
172+
128173 protected function assertUserActivities (array $ expected ): void {
129174 $ query = Server::get (IDBConnection::class)
130175 ->prepare ("SELECT `affecteduser` FROM `*PREFIX*activity` WHERE `type` = 'test' " );
0 commit comments